├── pyvim ├── commands │ ├── __init__.py │ ├── lexer.py │ ├── grammar.py │ ├── handler.py │ ├── completer.py │ ├── preview.py │ └── commands.py ├── entry_points │ ├── __init__.py │ └── run_pyvim.py ├── enums.py ├── __init__.py ├── io │ ├── __init__.py │ ├── base.py │ └── backends.py ├── __main__.py ├── help.py ├── welcome_message.py ├── rc_file.py ├── lexer.py ├── reporting.py ├── completion.py ├── style.py ├── key_bindings.py ├── editor_buffer.py ├── editor.py ├── window_arrangement.py └── layout.py ├── docs └── images │ ├── cjk.png │ ├── colorschemes.png │ ├── welcome-screen.png │ ├── window-layout.png │ ├── pyflakes-and-jedi.png │ └── editing-pyvim-source.png ├── .travis.yml ├── tests ├── test_window_arrangements.py ├── conftest.py └── test_substitute.py ├── .gitignore ├── setup.py ├── LICENSE ├── examples └── config │ └── pyvimrc ├── CHANGELOG └── README.rst /pyvim/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyvim/entry_points/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyvim/enums.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | -------------------------------------------------------------------------------- /pyvim/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | __version__ = '3.0.3' 4 | -------------------------------------------------------------------------------- /docs/images/cjk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-toolkit/pyvim/HEAD/docs/images/cjk.png -------------------------------------------------------------------------------- /docs/images/colorschemes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-toolkit/pyvim/HEAD/docs/images/colorschemes.png -------------------------------------------------------------------------------- /docs/images/welcome-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-toolkit/pyvim/HEAD/docs/images/welcome-screen.png -------------------------------------------------------------------------------- /docs/images/window-layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-toolkit/pyvim/HEAD/docs/images/window-layout.png -------------------------------------------------------------------------------- /docs/images/pyflakes-and-jedi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-toolkit/pyvim/HEAD/docs/images/pyflakes-and-jedi.png -------------------------------------------------------------------------------- /pyvim/io/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .base import * 4 | from .backends import * 5 | -------------------------------------------------------------------------------- /docs/images/editing-pyvim-source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prompt-toolkit/pyvim/HEAD/docs/images/editing-pyvim-source.png -------------------------------------------------------------------------------- /pyvim/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Make `python -m pyvim` an alias for running `pyvim`. 3 | """ 4 | from __future__ import unicode_literals 5 | from .entry_points.run_pyvim import run 6 | 7 | run() 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | 4 | matrix: 5 | include: 6 | - python: 3.6 7 | - python: 3.5 8 | - python: 2.7 9 | - python: pypy 10 | - python: pypy3 11 | 12 | install: 13 | - pip install . 14 | - pip install pytest 15 | - pip list 16 | 17 | script: 18 | - echo "$TRAVIS_PYTHON_VERSION" 19 | - cd tests 20 | - py.test 21 | -------------------------------------------------------------------------------- /pyvim/help.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | HELP_TEXT = """\ 4 | PyVim Help 5 | ========== 6 | 7 | PyVim is a Pure Python Vim Clone. 8 | 9 | 10 | Thanks to: 11 | - Pyflakes: the tool for checking Python source files for errors. 12 | - Jedi: the Python autocompletion library. 13 | - Pygments: Python syntax highlighter. 14 | - prompt_toolkit: the terminal UI toolkit. 15 | 16 | More help and documentation will follow.""" 17 | -------------------------------------------------------------------------------- /tests/test_window_arrangements.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.buffer import Buffer 4 | from pyvim.window_arrangement import EditorBuffer, VSplit 5 | 6 | 7 | def test_initial(window, tab_page): 8 | assert isinstance(tab_page.root, VSplit) 9 | assert tab_page.root == [window] 10 | 11 | 12 | def test_vsplit(editor, tab_page): 13 | # Create new buffer. 14 | eb = EditorBuffer(editor) 15 | 16 | # Insert in tab, by splitting. 17 | tab_page.vsplit(eb) 18 | 19 | assert isinstance(tab_page.root, VSplit) 20 | assert len(tab_page.root) == 2 21 | -------------------------------------------------------------------------------- /pyvim/commands/lexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer 4 | from prompt_toolkit.lexers import PygmentsLexer, SimpleLexer 5 | 6 | from pygments.lexers import BashLexer 7 | from .grammar import COMMAND_GRAMMAR 8 | 9 | __all__ = ( 10 | 'create_command_lexer', 11 | ) 12 | 13 | 14 | def create_command_lexer(): 15 | """ 16 | Lexer for highlighting of the command line. 17 | """ 18 | return GrammarLexer(COMMAND_GRAMMAR, lexers={ 19 | 'command': SimpleLexer('class:commandline.command'), 20 | 'location': SimpleLexer('class:commandline.location'), 21 | 'shell_command': PygmentsLexer(BashLexer), 22 | }) 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import pytest 4 | 5 | from prompt_toolkit.buffer import Buffer 6 | from prompt_toolkit.output import DummyOutput 7 | from prompt_toolkit.input import DummyInput 8 | from pyvim.editor import Editor 9 | from pyvim.window_arrangement import TabPage, EditorBuffer, Window 10 | 11 | 12 | @pytest.fixture 13 | def editor(): 14 | return Editor(output=DummyOutput(), input=DummyInput()) 15 | 16 | 17 | @pytest.fixture 18 | def editor_buffer(editor): 19 | return EditorBuffer(editor) 20 | 21 | 22 | @pytest.fixture 23 | def window(editor_buffer): 24 | return Window(editor_buffer) 25 | 26 | 27 | @pytest.fixture 28 | def tab_page(window): 29 | return TabPage(window) 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup, find_packages 4 | import pyvim 5 | 6 | with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as f: 7 | long_description = f.read() 8 | 9 | 10 | setup( 11 | name='pyvim', 12 | author='Jonathan Slenders', 13 | version=pyvim.__version__, 14 | license='BSD License', 15 | url='https://github.com/jonathanslenders/pyvim', 16 | description='Pure Python Vi Implementation', 17 | long_description=long_description, 18 | packages=find_packages('.'), 19 | install_requires = [ 20 | 'prompt_toolkit>=2.0.0,<3.1.0', 21 | 'six', 22 | 'pyflakes', # For Python error reporting. 23 | 'pygments', # For the syntax highlighting. 24 | 'docopt', # For command line arguments. 25 | ], 26 | entry_points={ 27 | 'console_scripts': [ 28 | 'pyvim = pyvim.entry_points.run_pyvim:run', 29 | ] 30 | }, 31 | ) 32 | -------------------------------------------------------------------------------- /pyvim/entry_points/run_pyvim.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | pyvim: Pure Python Vim clone. 4 | Usage: 5 | pyvim [-p] [-o] [-O] [-u ] [...] 6 | 7 | Options: 8 | -p : Open files in tab pages. 9 | -o : Split horizontally. 10 | -O : Split vertically. 11 | -u : Use this .pyvimrc file instead. 12 | """ 13 | from __future__ import unicode_literals 14 | import docopt 15 | import os 16 | 17 | from pyvim.editor import Editor 18 | from pyvim.rc_file import run_rc_file 19 | 20 | __all__ = ( 21 | 'run', 22 | ) 23 | 24 | 25 | def run(): 26 | a = docopt.docopt(__doc__) 27 | locations = a[''] 28 | in_tab_pages = a['-p'] 29 | hsplit = a['-o'] 30 | vsplit = a['-O'] 31 | pyvimrc = a['-u'] 32 | 33 | # Create new editor instance. 34 | editor = Editor() 35 | 36 | # Apply rc file. 37 | if pyvimrc: 38 | run_rc_file(editor, pyvimrc) 39 | else: 40 | default_pyvimrc = os.path.expanduser('~/.pyvimrc') 41 | 42 | if os.path.exists(default_pyvimrc): 43 | run_rc_file(editor, default_pyvimrc) 44 | 45 | # Load files and run. 46 | editor.load_initial_files(locations, in_tab_pages=in_tab_pages, 47 | hsplit=hsplit, vsplit=vsplit) 48 | editor.run() 49 | 50 | 51 | if __name__ == '__main__': 52 | run() 53 | -------------------------------------------------------------------------------- /pyvim/welcome_message.py: -------------------------------------------------------------------------------- 1 | """ 2 | The welcome message. This is displayed when the editor opens without any files. 3 | """ 4 | from __future__ import unicode_literals 5 | from prompt_toolkit.formatted_text.utils import fragment_list_len 6 | 7 | import prompt_toolkit 8 | import pyvim 9 | import platform 10 | import sys 11 | version = sys.version_info 12 | pyvim_version = pyvim.__version__ 13 | 14 | __all__ = ( 15 | 'WELCOME_MESSAGE_TOKENS', 16 | 'WELCOME_MESSAGE_WIDTH', 17 | 'WELCOME_MESSAGE_HEIGHT', 18 | ) 19 | 20 | WELCOME_MESSAGE_WIDTH = 36 21 | 22 | 23 | WELCOME_MESSAGE_TOKENS = [ 24 | ('class:title', 'PyVim - Pure Python Vi clone\n'), 25 | ('', 'Still experimental\n\n'), 26 | ('', 'version '), ('class:version', pyvim_version), 27 | ('', ', prompt_toolkit '), ('class:version', prompt_toolkit.__version__), 28 | ('', '\n'), 29 | ('', 'by Jonathan Slenders\n\n'), 30 | ('', 'type :q'), 31 | ('class:key', ''), 32 | ('', ' to exit\n'), 33 | ('', 'type :help'), 34 | ('class:key', ''), 35 | ('', ' or '), 36 | ('class:key', ''), 37 | ('', ' for help\n\n'), 38 | ('', 'All feedback is appreciated.\n\n'), 39 | ('class:pythonversion', ' %s %i.%i.%i ' % ( 40 | platform.python_implementation(), 41 | version[0], version[1], version[2])), 42 | ] 43 | 44 | WELCOME_MESSAGE_HEIGHT = ''.join(t[1] for t in WELCOME_MESSAGE_TOKENS).count('\n') + 1 45 | -------------------------------------------------------------------------------- /pyvim/io/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from abc import ABCMeta, abstractmethod 4 | import six 5 | 6 | __all__ = ( 7 | 'EditorIO', 8 | ) 9 | 10 | 11 | class EditorIO(six.with_metaclass(ABCMeta, object)): 12 | """ 13 | The I/O interface for editor buffers. 14 | 15 | It is for instance possible to pass an FTP I/O backend to EditorBuffer, to 16 | read/write immediately from an FTP server. Or a GZIP backend for files 17 | ending with .gz. 18 | """ 19 | @abstractmethod 20 | def can_open_location(cls, location): 21 | """ 22 | Return True when this I/O implementation is able to handle this `location`. 23 | """ 24 | return False 25 | 26 | @abstractmethod 27 | def exists(self, location): 28 | """ 29 | Return whether this location exists in this storage.. 30 | (If not, this is a new file.) 31 | """ 32 | return True 33 | 34 | @abstractmethod 35 | def read(self, location): 36 | """ 37 | Read file for storage. Returns (text, encoding tuple.) 38 | Can raise IOError. 39 | """ 40 | 41 | @abstractmethod 42 | def write(self, location, data, encoding='utf-8'): 43 | """ 44 | Write file to storage. 45 | Can raise IOError. 46 | """ 47 | 48 | def isdir(self, location): 49 | """ 50 | Return whether this location is a directory. 51 | """ 52 | return False 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Jonathan Slenders 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /pyvim/rc_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | `rc_file`s are configuration files. 3 | 4 | A pyvim configuration file is just a Python file that contains a global 5 | `configure` function. During startup, that function will be called with the 6 | editor instance as an argument. 7 | """ 8 | from __future__ import unicode_literals, print_function 9 | 10 | from .editor import Editor 11 | 12 | import six 13 | import os 14 | import traceback 15 | 16 | __all__ = ( 17 | 'run_rc_file', 18 | ) 19 | 20 | def _press_enter_to_continue(): 21 | """ Wait for the user to press enter. """ 22 | six.moves.input('\nPress ENTER to continue...') 23 | 24 | 25 | def run_rc_file(editor, rc_file): 26 | """ 27 | Run rc file. 28 | """ 29 | assert isinstance(editor, Editor) 30 | assert isinstance(rc_file, six.string_types) 31 | 32 | # Expand tildes. 33 | rc_file = os.path.expanduser(rc_file) 34 | 35 | # Check whether this file exists. 36 | if not os.path.exists(rc_file): 37 | print('Impossible to read %r' % rc_file) 38 | _press_enter_to_continue() 39 | return 40 | 41 | # Run the rc file in an empty namespace. 42 | try: 43 | namespace = {} 44 | 45 | with open(rc_file, 'r') as f: 46 | code = compile(f.read(), rc_file, 'exec') 47 | six.exec_(code, namespace, namespace) 48 | 49 | # Now we should have a 'configure' method in this namespace. We call this 50 | # method with editor as an argument. 51 | if 'configure' in namespace: 52 | namespace['configure'](editor) 53 | 54 | except Exception as e: 55 | # Handle possible exceptions in rc file. 56 | traceback.print_exc() 57 | _press_enter_to_continue() 58 | -------------------------------------------------------------------------------- /pyvim/commands/grammar.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.contrib.regular_languages.compiler import compile 4 | 5 | from .commands import get_commands_taking_locations 6 | 7 | 8 | #: The compiled grammar for the Vim command line. 9 | COMMAND_GRAMMAR = compile(r""" 10 | # Allow leading colons and whitespace. (They are ignored.) 11 | :* 12 | \s* 13 | ( 14 | # Substitute command 15 | ((?P\d+)(,(?P\d+))?)? (?Ps|substitute) \s* / (?P[^/]*) ( / (?P[^/]*) (?P /(g)? )? )? | 16 | 17 | # Commands accepting a location. 18 | (?P%(commands_taking_locations)s)(?P!?) \s+ (?P[^\s]+) | 19 | 20 | # Commands accepting a buffer. 21 | (?Pb|buffer)(?P!?) \s+ (?P[^\s]+) | 22 | 23 | # Jump to line numbers. 24 | (?P\d+) | 25 | 26 | # Set operation 27 | (?Pset) \s+ (?P[^\s=]+) 28 | (=(?P[^\s]+))? | 29 | 30 | # Colorscheme command 31 | (?Pcolorscheme) \s+ (?P[^\s]+) | 32 | 33 | # Shell command 34 | !(?P.*) | 35 | 36 | # Any other normal command. 37 | (?P[^\s!]+)(?P!?) | 38 | 39 | # Accept the empty input as well. (Ignores everything.) 40 | 41 | #(?Pcolorscheme.+) (?P[^\s]+) | 42 | ) 43 | 44 | # Allow trailing space. 45 | \s* 46 | """ % { 47 | 'commands_taking_locations': '|'.join(get_commands_taking_locations()), 48 | }) 49 | -------------------------------------------------------------------------------- /pyvim/lexer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.lexers import Lexer, SimpleLexer, PygmentsLexer 4 | from pygments.lexer import RegexLexer 5 | from pygments.token import Token 6 | 7 | __all__ = ( 8 | 'DocumentLexer', 9 | ) 10 | 11 | 12 | class DocumentLexer(Lexer): 13 | """ 14 | Lexer that depending on the filetype, uses another pygments lexer. 15 | """ 16 | def __init__(self, editor_buffer): 17 | self.editor_buffer = editor_buffer 18 | 19 | def lex_document(self, document): 20 | """ 21 | Call the lexer and return a get_tokens_for_line function. 22 | """ 23 | location = self.editor_buffer.location 24 | 25 | if location: 26 | if self.editor_buffer.in_file_explorer_mode: 27 | return PygmentsLexer(DirectoryListingLexer, sync_from_start=False).lex_document(document) 28 | 29 | return PygmentsLexer.from_filename(location, sync_from_start=False).lex_document(document) 30 | 31 | return SimpleLexer().lex_document(document) 32 | 33 | 34 | _DirectoryListing = Token.DirectoryListing 35 | 36 | class DirectoryListingLexer(RegexLexer): 37 | """ 38 | Highlighting of directory listings. 39 | """ 40 | name = 'directory-listing' 41 | tokens = { 42 | str('root'): [ # Conversion to `str` because of Pygments on Python 2. 43 | (r'^".*', _DirectoryListing.Header), 44 | 45 | (r'^\.\./$', _DirectoryListing.ParentDirectory), 46 | (r'^\./$', _DirectoryListing.CurrentDirectory), 47 | 48 | (r'^[^"].*/$', _DirectoryListing.Directory), 49 | (r'^[^"].*\.(txt|rst|md)$', _DirectoryListing.Textfile), 50 | (r'^[^"].*\.(py)$', _DirectoryListing.PythonFile), 51 | 52 | (r'^[^"].*\.(pyc|pyd)$', _DirectoryListing.Tempfile), 53 | (r'^\..*$', _DirectoryListing.Dotfile), 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /pyvim/commands/handler.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from .grammar import COMMAND_GRAMMAR 4 | from .commands import call_command_handler, has_command_handler, substitute 5 | 6 | __all__ = ( 7 | 'handle_command', 8 | ) 9 | 10 | 11 | def handle_command(editor, input_string): 12 | """ 13 | Handle commands entered on the Vi command line. 14 | """ 15 | # Match with grammar and extract variables. 16 | m = COMMAND_GRAMMAR.match(input_string) 17 | if m is None: 18 | return 19 | 20 | variables = m.variables() 21 | command = variables.get('command') 22 | go_to_line = variables.get('go_to_line') 23 | shell_command = variables.get('shell_command') 24 | range_start = variables.get('range_start') 25 | range_end = variables.get('range_end') 26 | search = variables.get('search') 27 | replace = variables.get('replace') 28 | flags = variables.get('flags', '') 29 | 30 | # Call command handler. 31 | 32 | if go_to_line is not None: 33 | # Handle go-to-line. 34 | _go_to_line(editor, go_to_line) 35 | 36 | elif shell_command is not None: 37 | # Handle shell commands. 38 | editor.application.run_system_command(shell_command) 39 | 40 | elif has_command_handler(command): 41 | # Handle other 'normal' commands. 42 | call_command_handler(command, editor, variables) 43 | 44 | elif command in ('s', 'substitute'): 45 | flags = flags.lstrip('/') 46 | substitute(editor, range_start, range_end, search, replace, flags) 47 | 48 | else: 49 | # For unknown commands, show error message. 50 | editor.show_message('Not an editor command: %s' % input_string) 51 | return 52 | 53 | # After execution of commands, make sure to update the layout and focus 54 | # stack. 55 | editor.sync_with_prompt_toolkit() 56 | 57 | 58 | def _go_to_line(editor, line): 59 | """ 60 | Move cursor to this line in the current buffer. 61 | """ 62 | b = editor.application.current_buffer 63 | b.cursor_position = b.document.translate_row_col_to_index(max(0, int(line) - 1), 0) 64 | -------------------------------------------------------------------------------- /pyvim/commands/completer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.completion import Completer, Completion 4 | from prompt_toolkit.completion import WordCompleter, PathCompleter 5 | from prompt_toolkit.contrib.completers.system import SystemCompleter 6 | from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter 7 | 8 | from .grammar import COMMAND_GRAMMAR 9 | from .commands import get_commands, SET_COMMANDS 10 | 11 | __all__ = ( 12 | 'create_command_completer', 13 | ) 14 | 15 | 16 | def create_command_completer(editor): 17 | commands = [c + ' ' for c in get_commands()] 18 | 19 | return GrammarCompleter(COMMAND_GRAMMAR, { 20 | 'command': WordCompleter(commands), 21 | 'location': PathCompleter(expanduser=True), 22 | 'set_option': WordCompleter(sorted(SET_COMMANDS)), 23 | 'buffer_name': BufferNameCompleter(editor), 24 | 'colorscheme': ColorSchemeCompleter(editor), 25 | 'shell_command': SystemCompleter(), 26 | }) 27 | 28 | 29 | class BufferNameCompleter(Completer): 30 | """ 31 | Complete on buffer names. 32 | It is sufficient when the input appears anywhere in the buffer name, to 33 | trigger a completion. 34 | """ 35 | def __init__(self, editor): 36 | self.editor = editor 37 | 38 | def get_completions(self, document, complete_event): 39 | text = document.text_before_cursor 40 | 41 | for eb in self.editor.window_arrangement.editor_buffers: 42 | location = eb.location 43 | 44 | if location is not None and text in location: 45 | yield Completion(location, start_position=-len(text), display=location) 46 | 47 | 48 | class ColorSchemeCompleter(Completer): 49 | """ 50 | Complete on the names of the color schemes that are currently known to the 51 | Editor instance. 52 | """ 53 | def __init__(self, editor): 54 | self.editor = editor 55 | 56 | def get_completions(self, document, complete_event): 57 | text = document.text_before_cursor 58 | 59 | for style_name in self.editor.styles: 60 | if style_name.startswith(text): 61 | yield Completion(style_name[len(text):], display=style_name) 62 | -------------------------------------------------------------------------------- /examples/config/pyvimrc: -------------------------------------------------------------------------------- 1 | # vim: set ft=python: 2 | """ 3 | Pyvim configuration. Save to file to: ~/.pyvimrc 4 | """ 5 | from prompt_toolkit.application import run_in_terminal 6 | from prompt_toolkit.filters import ViInsertMode 7 | from prompt_toolkit.key_binding.key_processor import KeyPress 8 | from prompt_toolkit.keys import Keys 9 | from subprocess import call 10 | import six 11 | 12 | __all__ = ( 13 | 'configure', 14 | ) 15 | 16 | 17 | def configure(editor): 18 | """ 19 | Configuration function. We receive a ``pyvim.editor.Editor`` instance as 20 | argument that we can manipulate in here. 21 | """ 22 | # Show line numbers by default. (:set number) 23 | editor.show_line_numbers = True 24 | 25 | # Highlight search. (:set hlsearch) 26 | editor.highlight_search = True 27 | 28 | # Case insensitive searching. (:set ignorecase) 29 | editor.ignore_case = True 30 | 31 | # Expand tab. (Pressing Tab will insert spaces.) 32 | editor.expand_tab = True # (:set expandtab) 33 | editor.tabstop = 4 # (:set tabstop=4) 34 | 35 | # Scroll offset (:set scrolloff) 36 | editor.scroll_offset = 2 37 | 38 | # Show tabs and trailing whitespace. (:set list) 39 | editor.display_unprintable_characters = True 40 | 41 | # Use Jedi for autocompletion of Python files. (:set jedi) 42 | editor.enable_jedi = True 43 | 44 | # Apply colorscheme. (:colorscheme emacs) 45 | editor.use_colorscheme('emacs') 46 | 47 | 48 | # Add custom key bindings: 49 | 50 | @editor.add_key_binding('j', 'j', filter=ViInsertMode()) 51 | def _(event): 52 | """ 53 | Typing 'jj' in Insert mode, should go back to navigation mode. 54 | 55 | (imap jj ) 56 | """ 57 | event.cli.key_processor.feed(KeyPress(Keys.Escape)) 58 | 59 | @editor.add_key_binding(Keys.F9) 60 | def save_and_execute_python_file(event): 61 | """ 62 | F9: Execute the current Python file. 63 | """ 64 | # Save buffer first. 65 | editor_buffer = editor.current_editor_buffer 66 | 67 | if editor_buffer is not None: 68 | if editor_buffer.location is None: 69 | editor.show_message("File doesn't have a filename. Please save first.") 70 | return 71 | else: 72 | editor_buffer.write() 73 | 74 | # Now run the Python interpreter. But use 75 | # `CommandLineInterface.run_in_terminal` to go to the background and 76 | # not destroy the window layout. 77 | def execute(): 78 | call(['python3', editor_buffer.location]) 79 | six.moves.input('Press enter to continue...') 80 | 81 | run_in_terminal(execute) 82 | -------------------------------------------------------------------------------- /pyvim/reporting.py: -------------------------------------------------------------------------------- 1 | """ 2 | Reporting. 3 | 4 | This is a way to highlight syntax errors in an open files. 5 | Reporters are run in an executor (in a thread) to ensure not blocking the 6 | input. 7 | 8 | Usage:: 9 | 10 | errors = report('location.py', Document('file content')) 11 | """ 12 | from __future__ import unicode_literals 13 | import pyflakes.api 14 | import string 15 | import six 16 | 17 | __all__ = ( 18 | 'report', 19 | ) 20 | 21 | 22 | class ReporterError(object): 23 | """ 24 | Error found by a reporter. 25 | """ 26 | def __init__(self, lineno, start_column, end_column, formatted_text): 27 | self.lineno = lineno # Zero based line number. 28 | self.start_column = start_column 29 | self.end_column = end_column 30 | self.formatted_text = formatted_text 31 | 32 | 33 | def report(location, document): 34 | """ 35 | Run reporter on document and return list of ReporterError instances. 36 | (Depending on the location it will or won't run anything.) 37 | 38 | Returns a list of `ReporterError`. 39 | """ 40 | assert isinstance(location, six.string_types) 41 | 42 | if location.endswith('.py'): 43 | return report_pyflakes(document) 44 | else: 45 | return [] 46 | 47 | 48 | WORD_CHARACTERS = string.ascii_letters + '0123456789_' 49 | 50 | 51 | def report_pyflakes(document): 52 | """ 53 | Run pyflakes on document and return list of ReporterError instances. 54 | """ 55 | # Run pyflakes on input. 56 | reporter = _FlakesReporter() 57 | pyflakes.api.check(document.text, '', reporter=reporter) 58 | 59 | def format_flake_message(message): 60 | return [ 61 | ('class:flakemessage.prefix', 'pyflakes:'), 62 | ('', ' '), 63 | ('class:flakemessage', message.message % message.message_args) 64 | ] 65 | 66 | def message_to_reporter_error(message): 67 | """ Turn pyflakes message into ReporterError. """ 68 | start_index = document.translate_row_col_to_index(message.lineno - 1, message.col) 69 | end_index = start_index 70 | while end_index < len(document.text) and document.text[end_index] in WORD_CHARACTERS: 71 | end_index += 1 72 | 73 | return ReporterError(lineno=message.lineno - 1, 74 | start_column=message.col, 75 | end_column=message.col + end_index - start_index, 76 | formatted_text=format_flake_message(message)) 77 | 78 | # Construct list of ReporterError instances. 79 | return [message_to_reporter_error(m) for m in reporter.messages] 80 | 81 | 82 | class _FlakesReporter(object): 83 | """ 84 | Reporter class to be passed to pyflakes.api.check. 85 | """ 86 | def __init__(self): 87 | self.messages = [] 88 | 89 | def unexpectedError(self, location, msg): 90 | pass 91 | 92 | def syntaxError(self, location, msg, lineno, offset, text): 93 | pass 94 | 95 | def flake(self, message): 96 | self.messages.append(message) 97 | -------------------------------------------------------------------------------- /pyvim/commands/preview.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from .grammar import COMMAND_GRAMMAR 3 | 4 | __all__ = ( 5 | 'CommandPreviewer', 6 | ) 7 | 8 | 9 | class CommandPreviewer(object): 10 | """ 11 | Already show the effect of Vi commands before enter is pressed. 12 | """ 13 | def __init__(self, editor): 14 | self.editor = editor 15 | 16 | def save(self): 17 | """ 18 | Back up current editor state. 19 | """ 20 | e = self.editor 21 | 22 | self._style = e.current_style 23 | self._show_line_numbers = e.show_line_numbers 24 | self._highlight_search = e.highlight_search 25 | self._show_ruler = e.show_ruler 26 | self._relative_number = e.relative_number 27 | self._cursorcolumn = e.cursorcolumn 28 | self._cursorline = e.cursorline 29 | self._colorcolumn = e.colorcolumn 30 | 31 | def restore(self): 32 | """ 33 | Focus of Vi command line lost, undo preview. 34 | """ 35 | e = self.editor 36 | 37 | e.current_style = self._style 38 | e.show_line_numbers = self._show_line_numbers 39 | e.highlight_search = self._highlight_search 40 | e.show_ruler = self._show_ruler 41 | e.relative_number = self._relative_number 42 | e.cursorcolumn = self._cursorcolumn 43 | e.cursorline = self._cursorline 44 | e.colorcolumn = self._colorcolumn 45 | 46 | def preview(self, input_string): 47 | """ 48 | Show effect of current Vi command. 49 | """ 50 | # First, the apply. 51 | self.restore() 52 | self._apply(input_string) 53 | 54 | def _apply(self, input_string): 55 | """ Apply command. """ 56 | e = self.editor 57 | 58 | # Parse command. 59 | m = COMMAND_GRAMMAR.match(input_string) 60 | if m is None: 61 | return 62 | 63 | variables = m.variables() 64 | 65 | command = variables.get('command') 66 | set_option = variables.get('set_option') 67 | 68 | # Preview colorschemes. 69 | if command == 'colorscheme': 70 | colorscheme = variables.get('colorscheme') 71 | if colorscheme: 72 | e.use_colorscheme(colorscheme) 73 | 74 | # Preview some set commands. 75 | if command == 'set': 76 | if set_option in ('hlsearch', 'hls'): 77 | e.highlight_search = True 78 | elif set_option in ('nohlsearch', 'nohls'): 79 | e.highlight_search = False 80 | elif set_option in ('nu', 'number'): 81 | e.show_line_numbers = True 82 | elif set_option in ('nonu', 'nonumber'): 83 | e.show_line_numbers = False 84 | elif set_option in ('ruler', 'ru'): 85 | e.show_ruler = True 86 | elif set_option in ('noruler', 'noru'): 87 | e.show_ruler = False 88 | elif set_option in ('relativenumber', 'rnu'): 89 | e.relative_number = True 90 | elif set_option in ('norelativenumber', 'nornu'): 91 | e.relative_number = False 92 | elif set_option in ('cursorline', 'cul'): 93 | e.cursorline = True 94 | elif set_option in ('cursorcolumn', 'cuc'): 95 | e.cursorcolumn = True 96 | elif set_option in ('nocursorline', 'nocul'): 97 | e.cursorline = False 98 | elif set_option in ('nocursorcolumn', 'nocuc'): 99 | e.cursorcolumn = False 100 | elif set_option in ('colorcolumn', 'cc'): 101 | value = variables.get('set_value', '') 102 | if value: 103 | e.colorcolumn = [ 104 | int(v) for v in value.split(',') if v.isdigit()] 105 | -------------------------------------------------------------------------------- /pyvim/completion.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from prompt_toolkit.completion import Completer, Completion 3 | 4 | import re 5 | import weakref 6 | 7 | __all__ = ( 8 | 'DocumentCompleter', 9 | ) 10 | 11 | 12 | class DocumentWordsCompleter(Completer): 13 | """ 14 | Completer that completes on words that appear already in the open document. 15 | """ 16 | def get_completions(self, document, complete_event): 17 | word_before_cursor = document.get_word_before_cursor() 18 | 19 | # Create a set of words that could be a possible completion. 20 | words = set() 21 | 22 | for w in re.split(r'\W', document.text): 23 | if len(w) > 1: 24 | if w.startswith(word_before_cursor) and w != word_before_cursor: 25 | words.add(w) 26 | 27 | # Yield Completion instances. 28 | for w in sorted(words): 29 | yield Completion(w, start_position=-len(word_before_cursor)) 30 | 31 | 32 | class DocumentCompleter(Completer): 33 | """ 34 | This is the general completer for EditorBuffer completions. 35 | Depending on the file type and settings, it selects another completer to 36 | call. 37 | """ 38 | def __init__(self, editor, editor_buffer): 39 | # (Weakrefs, they are already pointing to us.) 40 | self._editor_ref = weakref.ref(editor) 41 | self._editor_buffer_ref = weakref.ref(editor_buffer) 42 | 43 | def get_completions(self, document, complete_event): 44 | editor = self._editor_ref() 45 | location = self._editor_buffer_ref().location or '.txt' 46 | 47 | # Select completer. 48 | if location.endswith('.py') and editor.enable_jedi: 49 | completer = _PythonCompleter(location) 50 | else: 51 | completer = DocumentWordsCompleter() 52 | 53 | # Call completer. 54 | return completer.get_completions(document, complete_event) 55 | 56 | 57 | class _PythonCompleter(Completer): 58 | """ 59 | Wrapper around the Jedi completion engine. 60 | """ 61 | def __init__(self, location): 62 | self.location = location 63 | 64 | def get_completions(self, document, complete_event): 65 | script = self._get_jedi_script_from_document(document) 66 | if script: 67 | try: 68 | completions = script.completions() 69 | except TypeError: 70 | # Issue #9: bad syntax causes completions() to fail in jedi. 71 | # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 72 | pass 73 | except UnicodeDecodeError: 74 | # Issue #43: UnicodeDecodeError on OpenBSD 75 | # https://github.com/jonathanslenders/python-prompt-toolkit/issues/43 76 | pass 77 | except AttributeError: 78 | # Jedi issue #513: https://github.com/davidhalter/jedi/issues/513 79 | pass 80 | except ValueError: 81 | # Jedi issue: "ValueError: invalid \x escape" 82 | pass 83 | except KeyError: 84 | # Jedi issue: "KeyError: u'a_lambda'." 85 | # https://github.com/jonathanslenders/ptpython/issues/89 86 | pass 87 | except IOError: 88 | # Jedi issue: "IOError: No such file or directory." 89 | # https://github.com/jonathanslenders/ptpython/issues/71 90 | pass 91 | else: 92 | for c in completions: 93 | yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), 94 | display=c.name_with_symbols) 95 | 96 | def _get_jedi_script_from_document(self, document): 97 | import jedi # We keep this import in-line, to improve start-up time. 98 | # Importing Jedi is 'slow'. 99 | 100 | try: 101 | return jedi.Script( 102 | document.text, 103 | column=document.cursor_position_col, 104 | line=document.cursor_position_row + 1, 105 | path=self.location) 106 | except ValueError: 107 | # Invalid cursor position. 108 | # ValueError('`column` parameter is not in a valid range.') 109 | return None 110 | except AttributeError: 111 | # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65 112 | # See also: https://github.com/davidhalter/jedi/issues/508 113 | return None 114 | except IndexError: 115 | # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514 116 | return None 117 | except KeyError: 118 | # Workaround for a crash when the input is "u'", the start of a unicode string. 119 | return None 120 | 121 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 3.0.3: 2022-05-16 5 | ----------------- 6 | 7 | - Implemented basic :s[ubstitute] command and various related fixes. 8 | - Fixed license text in setup.py. 9 | 10 | 11 | 3.0.2: 2019-11-28 12 | ------------------ 13 | 14 | - Added missing dependency: 'six'. 15 | 16 | 17 | 3.0.1: 2019-11-28 18 | ------------------ 19 | 20 | - Upgrade to prompt_toolkit 3.0 21 | 22 | 23 | 2.0.24: 2019-01-27 24 | ------------------ 25 | 26 | - Improved the file explorer. 27 | 28 | 2.0.23: 2018-09-30 29 | ------------------ 30 | 31 | - Implemented "breakindent" option. 32 | - Implemented "temporary navigation mode". 33 | 34 | 2.0.22: 2018-06-03 35 | ----------------- 36 | 37 | - Small fix: don't include default input processors from prompt_toolkit. 38 | 39 | 40 | 2.0.1: 2018-06-02 41 | ----------------- 42 | 43 | Upgrade to prompt_toolkit 2.0 44 | 45 | Edit: By accident, this was uploaded as 2.0.21. 46 | 47 | 48 | 0.0.21: 2017-08-08 49 | ------------------ 50 | 51 | - Use load_key_bindings instead of KeyBindingManager (fixes compatibility with 52 | latest prompt_toolkit 1.0) 53 | 54 | 55 | 0.0.20: 2016-10-16 56 | ------------------- 57 | 58 | - Added support for inserting before/after visual block. 59 | - Better Jedi integration for completion of Python files. 60 | - Don't depend on ptpython code anymore. 61 | 62 | Upgrade to prompt_toolkit==1.0.8 63 | 64 | 65 | 0.0.19: 2016-08-04 66 | ------------------ 67 | 68 | - Take output encoding ($LANG) into account in several places. 69 | 70 | Upgrade to prompt_toolkit==1.0.4 71 | 72 | 73 | 0.0.18: 2016-05-09 74 | ------------------ 75 | 76 | Upgrade to ptpython==0.34. 77 | 78 | 79 | 0.0.17: 2016-05-05 80 | ----------------- 81 | 82 | Upgrade to prompt_toolkit==1.0.0 and ptpython==0.32. 83 | 84 | - Added colorcolumn, cursorcolumn and cursorline commands. 85 | - Added cul,nocul,cuc,nocuc commands. 86 | - Added tildeop command. 87 | - Fixes bug in ~ expension in :e command. 88 | 89 | 90 | 0.0.16: 2016-03-14 91 | ----------------- 92 | 93 | Upgrade to prompt_toolkit==0.60 and ptpython==0.31. 94 | 95 | 96 | 0.0.15: 2016-02-27 97 | ----------------- 98 | 99 | Upgrade to prompt_toolkit==0.59 and ptpython==0.30. 100 | 101 | 102 | 0.0.14: 2016-02-24 103 | ----------------- 104 | 105 | Upgrade to prompt_toolkit==0.58 and ptpython==0.29. 106 | 107 | 108 | 0.0.13: 2016-01-04 109 | ----------------- 110 | 111 | Upgrade to prompt_toolkit==0.57 and ptpython==0.28. 112 | 113 | 114 | 0.0.12: 2016-01-03 115 | ----------------- 116 | 117 | Upgrade to prompt_toolkit==0.56 and ptpython==0.27. 118 | 119 | New features: 120 | - Visual block selection type. 121 | - Handle mouse events on tabs. 122 | - Focus window on click. 123 | - Show the current document in the titlebar. 124 | 125 | Fixes: 126 | - Make sure that 'pyvim -u ...' alsa works on Python 2. 127 | 128 | 129 | 0.0.11: 2015-10-29 130 | ----------------- 131 | 132 | Upgrade to prompt_toolkit==0.54 and ptpython==0.25. 133 | 134 | 135 | 0.0.10: 2015-09-24 136 | ----------------- 137 | 138 | Upgrade to prompt_toolkit==0.52 and ptpython==0.24. 139 | 140 | 141 | 0.0.9: 2015-09-24 142 | ----------------- 143 | 144 | Upgrade to prompt_toolkit==0.51 and ptpython==0.23. 145 | 146 | 147 | 0.0.8: 2015-08-08 148 | ----------------- 149 | 150 | Upgrade to prompt_toolkit==0.46 and ptpython==0.21. 151 | 152 | 153 | 0.0.7: 2015-06-30 154 | ----------------- 155 | 156 | Upgrade to prompt_toolkit==0.45 and ptpython==0.20. 157 | 158 | 159 | 0.0.6: 2015-06-30 160 | ----------------- 161 | 162 | Upgrade to prompt_toolkit==0.44 and ptpython==0.19. 163 | 164 | New features: 165 | - Added __main__: allow "python -m pyvim". 166 | 167 | 168 | 0.0.5: 2015-06-22 169 | ----------------- 170 | 171 | Upgrade to prompt_toolkit==0.41 and ptpython==0.15. 172 | 173 | 174 | 0.0.4: 2015-05-31 175 | ----------------- 176 | 177 | New features: 178 | - Upgrade to prompt_toolkit==0.38: 179 | Several new key bindings bug fixes and faster pasting. 180 | 181 | 0.0.3: 2015-05-07 182 | ----------------- 183 | 184 | New features: 185 | - shortcuts for split and vsplit. 186 | - Page up/down key bindings. 187 | - ControlW n/v key bindings for splitting windows. 188 | - Custom I/O backends. (For opening of .gz files and http:// urls.) 189 | - Implemented ControlE/ControlY/ControlD/ControlU for scrolling. 190 | - Implemented scroll offset. 191 | - Added :bd, :cq and :open 192 | - Better handling of exclamation mark in commands. 193 | 194 | Bug fixes: 195 | - NameErrors in .pyvimrc example. 196 | - ControlF shortcut. 197 | - Solves :q issue. 198 | - Fixed ControlT 199 | 200 | 0.0.2: 2015-04-26 201 | ----------------- 202 | 203 | First public release: 204 | - Working layouts: horizontal/vertical splits, tab pages, etc... 205 | - Many commands + key bindings. 206 | - Jedi and Pyflakes integration. 207 | - Reading and writing of files. 208 | - Many other stuff. 209 | 210 | 211 | 2015-01-25 212 | ---------- 213 | 214 | First working proof of concept. 215 | -------------------------------------------------------------------------------- /pyvim/io/backends.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import codecs 4 | import gzip 5 | import os 6 | import six 7 | from six.moves import urllib 8 | 9 | from .base import EditorIO 10 | 11 | __all__ = ( 12 | 'FileIO', 13 | 'GZipFileIO', 14 | 'DirectoryIO', 15 | 'HttpIO', 16 | ) 17 | 18 | 19 | ENCODINGS = ['utf-8', 'latin-1'] 20 | 21 | 22 | class FileIO(EditorIO): 23 | """ 24 | I/O backend for the native file system. 25 | """ 26 | def can_open_location(cls, location): 27 | # We can handle all local files. 28 | return '://' not in location and not os.path.isdir(location) 29 | 30 | def exists(self, location): 31 | return os.path.exists(os.path.expanduser(location)) 32 | 33 | def read(self, location): 34 | """ 35 | Read file from disk. 36 | """ 37 | location = os.path.expanduser(location) 38 | 39 | # Try to open this file, using different encodings. 40 | for e in ENCODINGS: 41 | try: 42 | with codecs.open(location, 'r', e) as f: 43 | return f.read(), e 44 | except UnicodeDecodeError: 45 | pass # Try next codec. 46 | 47 | # Unable to open. 48 | raise Exception('Unable to open file: %r' % location) 49 | 50 | def write(self, location, text, encoding): 51 | """ 52 | Write file to disk. 53 | """ 54 | location = os.path.expanduser(location) 55 | 56 | with codecs.open(location, 'w', encoding) as f: 57 | f.write(text) 58 | 59 | 60 | class GZipFileIO(EditorIO): 61 | """ 62 | I/O backend for gzip files. 63 | 64 | It is possible to edit this file as if it were not compressed. 65 | The read and write call will decompress and compress transparently. 66 | """ 67 | def can_open_location(cls, location): 68 | return FileIO().can_open_location(location) and location.endswith('.gz') 69 | 70 | def exists(self, location): 71 | return FileIO().exists(location) 72 | 73 | def read(self, location): 74 | location = os.path.expanduser(location) 75 | 76 | with gzip.open(location, 'rb') as f: 77 | data = f.read() 78 | return _auto_decode(data) 79 | 80 | def write(self, location, text, encoding): 81 | """ 82 | Write file to disk. 83 | """ 84 | location = os.path.expanduser(location) 85 | 86 | with gzip.open(location, 'wb') as f: 87 | f.write(text.encode(encoding)) 88 | 89 | 90 | class DirectoryIO(EditorIO): 91 | """ 92 | Create a textual listing of the directory content. 93 | """ 94 | def can_open_location(cls, location): 95 | # We can handle all local directories. 96 | return '://' not in location and os.path.isdir(location) 97 | 98 | def exists(self, location): 99 | return os.path.isdir(location) 100 | 101 | def read(self, directory): 102 | # Read content. 103 | content = sorted(os.listdir(directory)) 104 | directories = [] 105 | files = [] 106 | 107 | for f in content: 108 | if os.path.isdir(os.path.join(directory, f)): 109 | directories.append(f) 110 | else: 111 | files.append(f) 112 | 113 | # Construct output. 114 | result = [] 115 | result.append('" ==================================\n') 116 | result.append('" Directory Listing\n') 117 | result.append('" %s\n' % os.path.abspath(directory)) 118 | result.append('" Quick help: -: go up dir\n') 119 | result.append('" ==================================\n') 120 | result.append('../\n') 121 | result.append('./\n') 122 | 123 | for d in directories: 124 | result.append('%s/\n' % d) 125 | 126 | for f in files: 127 | result.append('%s\n' % f) 128 | 129 | return ''.join(result), 'utf-8' 130 | 131 | def write(self, location, text, encoding): 132 | raise NotImplementedError('Cannot write to directory.') 133 | 134 | def isdir(self, location): 135 | return True 136 | 137 | 138 | class HttpIO(EditorIO): 139 | """ 140 | I/O backend that reads from HTTP. 141 | """ 142 | def can_open_location(cls, location): 143 | # We can handle all local directories. 144 | return location.startswith('http://') or location.startswith('https://') 145 | 146 | def exists(self, location): 147 | return NotImplemented # We don't know. 148 | 149 | def read(self, location): 150 | # Do Http request. 151 | bytes = urllib.request.urlopen(location).read() 152 | 153 | # Return decoded. 154 | return _auto_decode(bytes) 155 | 156 | def write(self, location, text, encoding): 157 | raise NotImplementedError('Cannot write to HTTP.') 158 | 159 | 160 | def _auto_decode(data): 161 | """ 162 | Decode bytes. Return a (text, encoding) tuple. 163 | """ 164 | assert isinstance(data, six.binary_type) 165 | 166 | for e in ENCODINGS: 167 | try: 168 | return data.decode(e), e 169 | except UnicodeDecodeError: 170 | pass 171 | 172 | return data.decode('utf-8', 'ignore') 173 | -------------------------------------------------------------------------------- /tests/test_substitute.py: -------------------------------------------------------------------------------- 1 | from pyvim.commands.handler import handle_command 2 | 3 | sample_text = """ 4 | Roses are red, 5 | Violets are blue, 6 | Sugar is sweet, 7 | And so are you. 8 | """.lstrip() 9 | 10 | def given_sample_text(editor_buffer, text=None): 11 | editor = editor_buffer.editor 12 | editor.window_arrangement._add_editor_buffer(editor_buffer) 13 | editor_buffer.buffer.text = text or sample_text 14 | editor.sync_with_prompt_toolkit() 15 | 16 | 17 | def given_cursor_position(editor_buffer, line_number, column=0): 18 | editor_buffer.buffer.cursor_position = \ 19 | editor_buffer.buffer.document.translate_row_col_to_index(line_number - 1, column) 20 | 21 | 22 | def test_substitute_current_line(editor, editor_buffer): 23 | given_sample_text(editor_buffer) 24 | given_cursor_position(editor_buffer, 2) 25 | 26 | handle_command(editor, ':s/s are/ is') 27 | 28 | assert 'Roses are red,' in editor_buffer.buffer.text 29 | assert 'Violet is blue,' in editor_buffer.buffer.text 30 | assert 'And so are you.' in editor_buffer.buffer.text 31 | assert editor_buffer.buffer.cursor_position \ 32 | == editor_buffer.buffer.text.index('Violet') 33 | 34 | 35 | def test_substitute_single_line(editor, editor_buffer): 36 | given_sample_text(editor_buffer) 37 | given_cursor_position(editor_buffer, 1) 38 | 39 | handle_command(editor, ':2s/s are/ is') 40 | 41 | assert 'Roses are red,' in editor_buffer.buffer.text 42 | assert 'Violet is blue,' in editor_buffer.buffer.text 43 | assert 'And so are you.' in editor_buffer.buffer.text 44 | assert editor_buffer.buffer.cursor_position \ 45 | == editor_buffer.buffer.text.index('Violet') 46 | 47 | 48 | def test_substitute_range(editor, editor_buffer): 49 | given_sample_text(editor_buffer) 50 | given_cursor_position(editor_buffer, 1) 51 | 52 | handle_command(editor, ':1,3s/s are/ is') 53 | 54 | assert 'Rose is red,' in editor_buffer.buffer.text 55 | assert 'Violet is blue,' in editor_buffer.buffer.text 56 | assert 'And so are you.' in editor_buffer.buffer.text 57 | # FIXME: vim would have set the cursor position on last substituted line 58 | # but we set the cursor position on the end_range even when there 59 | # is not substitution there 60 | # assert editor_buffer.buffer.cursor_position \ 61 | # == editor_buffer.buffer.text.index('Violet') 62 | assert editor_buffer.buffer.cursor_position \ 63 | == editor_buffer.buffer.text.index('Sugar') 64 | 65 | 66 | def test_substitute_range_boundaries(editor, editor_buffer): 67 | given_sample_text(editor_buffer, 'Violet\n' * 4) 68 | 69 | handle_command(editor, ':2,3s/Violet/Rose') 70 | 71 | assert 'Violet\nRose\nRose\nViolet\n' in editor_buffer.buffer.text 72 | 73 | 74 | def test_substitute_from_search_history(editor, editor_buffer): 75 | given_sample_text(editor_buffer) 76 | editor.application.current_search_state.text = 'blue' 77 | 78 | handle_command(editor, ':1,3s//pretty') 79 | assert 'Violets are pretty,' in editor_buffer.buffer.text 80 | 81 | 82 | def test_substitute_from_substitute_search_history(editor, editor_buffer): 83 | given_sample_text(editor_buffer, 'Violet is Violet\n') 84 | 85 | handle_command(editor, ':s/Violet/Rose') 86 | assert 'Rose is Violet' in editor_buffer.buffer.text 87 | 88 | handle_command(editor, ':s//Lily') 89 | assert 'Rose is Lily' in editor_buffer.buffer.text 90 | 91 | 92 | def test_substitute_with_repeat_last_substitution(editor, editor_buffer): 93 | given_sample_text(editor_buffer, 'Violet is Violet\n') 94 | editor.application.current_search_state.text = 'Lily' 95 | 96 | handle_command(editor, ':s/Violet/Rose') 97 | assert 'Rose is Violet' in editor_buffer.buffer.text 98 | 99 | handle_command(editor, ':s') 100 | assert 'Rose is Rose' in editor_buffer.buffer.text 101 | 102 | 103 | def test_substitute_without_replacement_text(editor, editor_buffer): 104 | given_sample_text(editor_buffer, 'Violet Violet Violet \n') 105 | editor.application.current_search_state.text = 'Lily' 106 | 107 | handle_command(editor, ':s/Violet/') 108 | assert ' Violet Violet \n' in editor_buffer.buffer.text 109 | 110 | handle_command(editor, ':s/Violet') 111 | assert ' Violet \n' in editor_buffer.buffer.text 112 | 113 | handle_command(editor, ':s/') 114 | assert ' \n' in editor_buffer.buffer.text 115 | 116 | 117 | def test_substitute_with_repeat_last_substitution_without_previous_substitution(editor, editor_buffer): 118 | original_text = 'Violet is blue\n' 119 | given_sample_text(editor_buffer, original_text) 120 | 121 | handle_command(editor, ':s') 122 | assert original_text in editor_buffer.buffer.text 123 | 124 | editor.application.current_search_state.text = 'blue' 125 | 126 | handle_command(editor, ':s') 127 | assert 'Violet is \n' in editor_buffer.buffer.text 128 | 129 | 130 | def test_substitute_flags_empty_flags(editor, editor_buffer): 131 | given_sample_text(editor_buffer, 'Violet is Violet\n') 132 | handle_command(editor, ':s/Violet/Rose/') 133 | assert 'Rose is Violet' in editor_buffer.buffer.text 134 | 135 | 136 | def test_substitute_flags_g(editor, editor_buffer): 137 | given_sample_text(editor_buffer, 'Violet is Violet\n') 138 | handle_command(editor, ':s/Violet/Rose/g') 139 | assert 'Rose is Rose' in editor_buffer.buffer.text 140 | -------------------------------------------------------------------------------- /pyvim/style.py: -------------------------------------------------------------------------------- 1 | """ 2 | The styles, for the colorschemes. 3 | """ 4 | from __future__ import unicode_literals 5 | from prompt_toolkit.styles import Style, merge_styles 6 | from prompt_toolkit.styles.pygments import style_from_pygments_cls 7 | 8 | from pygments.styles import get_all_styles, get_style_by_name 9 | 10 | __all__ = ( 11 | 'generate_built_in_styles', 12 | 'get_editor_style_by_name', 13 | ) 14 | 15 | 16 | def get_editor_style_by_name(name): 17 | """ 18 | Get Style class. 19 | This raises `pygments.util.ClassNotFound` when there is no style with this 20 | name. 21 | """ 22 | if name == 'vim': 23 | vim_style = Style.from_dict(default_vim_style) 24 | else: 25 | vim_style = style_from_pygments_cls(get_style_by_name(name)) 26 | 27 | return merge_styles([ 28 | vim_style, 29 | Style.from_dict(style_extensions), 30 | ]) 31 | 32 | 33 | def generate_built_in_styles(): 34 | """ 35 | Return a mapping from style names to their classes. 36 | """ 37 | return dict((name, get_editor_style_by_name(name)) for name in get_all_styles()) 38 | 39 | 40 | style_extensions = { 41 | # Toolbar colors. 42 | 'toolbar.status': '#ffffff bg:#444444', 43 | 'toolbar.status.cursorposition': '#bbffbb bg:#444444', 44 | 'toolbar.status.percentage': '#ffbbbb bg:#444444', 45 | 46 | # Flakes color. 47 | 'flakeserror': 'bg:#ff4444 #ffffff', 48 | 49 | # Flake messages 50 | 'flakemessage.prefix': 'bg:#ff8800 #ffffff', 51 | 'flakemessage': '#886600', 52 | 53 | # Highlighting for the text in the command bar. 54 | 'commandline.command': 'bold', 55 | 'commandline.location': 'bg:#bbbbff #000000', 56 | 57 | # Frame borders (for between vertical splits.) 58 | 'frameborder': 'bold', #bg:#88aa88 #ffffff', 59 | 60 | # Messages 61 | 'message': 'bg:#bbee88 #222222', 62 | 63 | # Welcome message 64 | 'welcome title': 'underline', 65 | 'welcome version': '#8800ff', 66 | 'welcome key': '#0000ff', 67 | 'welcome pythonversion': 'bg:#888888 #ffffff', 68 | 69 | # Tabs 70 | 'tabbar': 'noinherit reverse', 71 | 'tabbar.tab': 'underline', 72 | 'tabbar.tab.active': 'bold noinherit', 73 | 74 | # Arg count. 75 | 'arg': 'bg:#cccc44 #000000', 76 | 77 | # Buffer list 78 | 'bufferlist': 'bg:#aaddaa #000000', 79 | 'bufferlist title': 'underline', 80 | 'bufferlist lineno': '#666666', 81 | 'bufferlist active': 'bg:#ccffcc', 82 | 'bufferlist active.lineno': '#666666', 83 | 'bufferlist searchmatch': 'bg:#eeeeaa', 84 | 85 | # Completions toolbar. 86 | 'completions-toolbar': 'bg:#aaddaa #000000', 87 | 'completions-toolbar.arrow': 'bg:#aaddaa #000000 bold', 88 | 'completions-toolbar completion': 'bg:#aaddaa #000000', 89 | 'completions-toolbar current-completion': 'bg:#444444 #ffffff', 90 | 91 | # Soft wrap. 92 | 'soft-wrap': '#888888', 93 | 94 | # Directory listing style. 95 | 'pygments.directorylisting.header': '#4444ff', 96 | 'pygments.directorylisting.directory': '#ff4444 bold', 97 | 'pygments.directorylisting.currentdirectory': '#888888', 98 | 'pygments.directorylisting.parentdirectory': '#888888', 99 | 'pygments.directorylisting.tempfile': '#888888', 100 | 'pygments.directorylisting.dotfile': '#888888', 101 | 'pygments.directorylisting.pythonfile': '#8800ff', 102 | 'pygments.directorylisting.textfile': '#aaaa00', 103 | } 104 | 105 | 106 | # Default 'vim' color scheme. Taken from the Pygments Vim colorscheme, but 107 | # modified to use mainly ANSI colors. 108 | default_vim_style = { 109 | 'pygments': '', 110 | 'pygments.whitespace': '', 111 | 'pygments.comment': 'ansiblue', 112 | 'pygments.comment.preproc': 'ansiyellow', 113 | 'pygments.comment.special': 'bold', 114 | 115 | 'pygments.keyword': '#999900', 116 | 'pygments.keyword.declaration': 'ansigreen', 117 | 'pygments.keyword.namespace': 'ansimagenta', 118 | 'pygments.keyword.pseudo': '', 119 | 'pygments.keyword.type': 'ansigreen', 120 | 121 | 'pygments.operator': '', 122 | 'pygments.operator.word': '', 123 | 124 | 'pygments.name': '', 125 | 'pygments.name.class': 'ansicyan', 126 | 'pygments.name.builtin': 'ansicyan', 127 | 'pygments.name.exception': '', 128 | 'pygments.name.variable': 'ansicyan', 129 | 'pygments.name.function': 'ansicyan', 130 | 131 | 'pygments.literal': 'ansired', 132 | 'pygments.string': 'ansired', 133 | 'pygments.string.doc': '', 134 | 'pygments.number': 'ansimagenta', 135 | 136 | 'pygments.generic.heading': 'bold ansiblue', 137 | 'pygments.generic.subheading': 'bold ansimagenta', 138 | 'pygments.generic.deleted': 'ansired', 139 | 'pygments.generic.inserted': 'ansigreen', 140 | 'pygments.generic.error': 'ansibrightred', 141 | 'pygments.generic.emph': 'italic', 142 | 'pygments.generic.strong': 'bold', 143 | 'pygments.generic.prompt': 'bold ansiblue', 144 | 'pygments.generic.output': 'ansigray', 145 | 'pygments.generic.traceback': '#04d', 146 | 147 | 'pygments.error': 'border:ansired' 148 | } 149 | -------------------------------------------------------------------------------- /pyvim/key_bindings.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from prompt_toolkit.application import get_app 4 | from prompt_toolkit.filters import Condition, has_focus, vi_insert_mode, vi_navigation_mode 5 | from prompt_toolkit.key_binding import KeyBindings 6 | 7 | import os 8 | 9 | __all__ = ( 10 | 'create_key_bindings', 11 | ) 12 | 13 | 14 | def _current_window_for_event(event): 15 | """ 16 | Return the `Window` for the currently focussed Buffer. 17 | """ 18 | return event.app.layout.current_window 19 | 20 | 21 | def create_key_bindings(editor): 22 | """ 23 | Create custom key bindings. 24 | 25 | This starts with the key bindings, defined by `prompt-toolkit`, but adds 26 | the ones which are specific for the editor. 27 | """ 28 | kb = KeyBindings() 29 | 30 | # Filters. 31 | @Condition 32 | def vi_buffer_focussed(): 33 | app = get_app() 34 | if app.layout.has_focus(editor.search_buffer) or app.layout.has_focus(editor.command_buffer): 35 | return False 36 | return True 37 | 38 | in_insert_mode = vi_insert_mode & vi_buffer_focussed 39 | in_navigation_mode = vi_navigation_mode & vi_buffer_focussed 40 | 41 | @kb.add('c-t') 42 | def _(event): 43 | """ 44 | Override default behaviour of prompt-toolkit. 45 | (Control-T will swap the last two characters before the cursor, because 46 | that's what readline does.) 47 | """ 48 | pass 49 | 50 | @kb.add('c-t', filter=in_insert_mode) 51 | def indent_line(event): 52 | """ 53 | Indent current line. 54 | """ 55 | b = event.application.current_buffer 56 | 57 | # Move to start of line. 58 | pos = b.document.get_start_of_line_position(after_whitespace=True) 59 | b.cursor_position += pos 60 | 61 | # Insert tab. 62 | if editor.expand_tab: 63 | b.insert_text(' ') 64 | else: 65 | b.insert_text('\t') 66 | 67 | # Restore cursor. 68 | b.cursor_position -= pos 69 | 70 | @kb.add('c-r', filter=in_navigation_mode, save_before=(lambda e: False)) 71 | def redo(event): 72 | """ 73 | Redo. 74 | """ 75 | event.app.current_buffer.redo() 76 | 77 | @kb.add(':', filter=in_navigation_mode) 78 | def enter_command_mode(event): 79 | """ 80 | Entering command mode. 81 | """ 82 | editor.enter_command_mode() 83 | 84 | @kb.add('tab', filter=vi_insert_mode & 85 | ~has_focus(editor.command_buffer) & whitespace_before_cursor_on_line) 86 | def autocomplete_or_indent(event): 87 | """ 88 | When the 'tab' key is pressed with only whitespace character before the 89 | cursor, do autocompletion. Otherwise, insert indentation. 90 | """ 91 | b = event.app.current_buffer 92 | if editor.expand_tab: 93 | b.insert_text(' ') 94 | else: 95 | b.insert_text('\t') 96 | 97 | @kb.add('escape', filter=has_focus(editor.command_buffer)) 98 | @kb.add('c-c', filter=has_focus(editor.command_buffer)) 99 | @kb.add('backspace', 100 | filter=has_focus(editor.command_buffer) & Condition(lambda: editor.command_buffer.text == '')) 101 | def leave_command_mode(event): 102 | """ 103 | Leaving command mode. 104 | """ 105 | editor.leave_command_mode() 106 | 107 | @kb.add('c-w', 'c-w', filter=in_navigation_mode) 108 | def focus_next_window(event): 109 | editor.window_arrangement.cycle_focus() 110 | editor.sync_with_prompt_toolkit() 111 | 112 | @kb.add('c-w', 'n', filter=in_navigation_mode) 113 | def horizontal_split(event): 114 | """ 115 | Split horizontally. 116 | """ 117 | editor.window_arrangement.hsplit(None) 118 | editor.sync_with_prompt_toolkit() 119 | 120 | @kb.add('c-w', 'v', filter=in_navigation_mode) 121 | def vertical_split(event): 122 | """ 123 | Split vertically. 124 | """ 125 | editor.window_arrangement.vsplit(None) 126 | editor.sync_with_prompt_toolkit() 127 | 128 | @kb.add('g', 't', filter=in_navigation_mode) 129 | def focus_next_tab(event): 130 | editor.window_arrangement.go_to_next_tab() 131 | editor.sync_with_prompt_toolkit() 132 | 133 | @kb.add('g', 'T', filter=in_navigation_mode) 134 | def focus_previous_tab(event): 135 | editor.window_arrangement.go_to_previous_tab() 136 | editor.sync_with_prompt_toolkit() 137 | 138 | @kb.add('f1') 139 | def show_help(event): 140 | editor.show_help() 141 | 142 | @Condition 143 | def in_file_explorer_mode(): 144 | return bool(editor.current_editor_buffer and 145 | editor.current_editor_buffer.in_file_explorer_mode) 146 | 147 | @kb.add('enter', filter=in_file_explorer_mode) 148 | def open_path(event): 149 | """ 150 | Open file/directory in file explorer mode. 151 | """ 152 | name_under_cursor = event.current_buffer.document.current_line 153 | new_path = os.path.normpath(os.path.join( 154 | editor.current_editor_buffer.location, name_under_cursor)) 155 | 156 | editor.window_arrangement.open_buffer( 157 | new_path, show_in_current_window=True) 158 | editor.sync_with_prompt_toolkit() 159 | 160 | @kb.add('-', filter=in_file_explorer_mode) 161 | def to_parent_directory(event): 162 | new_path = os.path.normpath(os.path.join( 163 | editor.current_editor_buffer.location, '..')) 164 | 165 | editor.window_arrangement.open_buffer( 166 | new_path, show_in_current_window=True) 167 | editor.sync_with_prompt_toolkit() 168 | 169 | return kb 170 | 171 | 172 | @Condition 173 | def whitespace_before_cursor_on_line(): 174 | """ 175 | Filter which evaluates to True when the characters before the cursor are 176 | whitespace, or we are at the start of te line. 177 | """ 178 | b = get_app().current_buffer 179 | before_cursor = b.document.current_line_before_cursor 180 | 181 | return bool(not before_cursor or before_cursor[-1].isspace()) 182 | -------------------------------------------------------------------------------- /pyvim/editor_buffer.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from prompt_toolkit.application.current import get_app 3 | from prompt_toolkit.buffer import Buffer 4 | from prompt_toolkit.document import Document 5 | from prompt_toolkit import __version__ as ptk_version 6 | 7 | from pyvim.completion import DocumentCompleter 8 | from pyvim.reporting import report 9 | 10 | from six import string_types 11 | 12 | import os 13 | import weakref 14 | 15 | PTK3 = ptk_version.startswith('3.') 16 | 17 | if PTK3: 18 | from asyncio import get_event_loop 19 | else: 20 | from prompt_toolkit.eventloop import call_from_executor, run_in_executor 21 | 22 | __all__ = ( 23 | 'EditorBuffer', 24 | ) 25 | 26 | 27 | class EditorBuffer(object): 28 | """ 29 | Wrapper around a `prompt-toolkit` buffer. 30 | 31 | A 'prompt-toolkit' `Buffer` doesn't know anything about files, changes, 32 | etc... This wrapper contains the necessary data for the editor. 33 | """ 34 | def __init__(self, editor, location=None, text=None): 35 | assert location is None or isinstance(location, string_types) 36 | assert text is None or isinstance(text, string_types) 37 | assert not (location and text) 38 | 39 | self._editor_ref = weakref.ref(editor) 40 | self.location = location 41 | self.encoding = 'utf-8' 42 | 43 | #: is_new: True when this file does not yet exist in the storage. 44 | self.is_new = True 45 | 46 | # Empty if not in file explorer mode, directory path otherwise. 47 | self.isdir = False 48 | 49 | # Read text. 50 | if location: 51 | text = self._read(location) 52 | else: 53 | text = text or '' 54 | 55 | self._file_content = text 56 | 57 | # Create Buffer. 58 | self.buffer = Buffer( 59 | multiline=True, 60 | completer=DocumentCompleter(editor, self), 61 | document=Document(text, 0), 62 | on_text_changed=lambda _: self.run_reporter()) 63 | 64 | # List of reporting errors. 65 | self.report_errors = [] 66 | self._reporter_is_running = False 67 | 68 | @property 69 | def editor(self): 70 | """ Back reference to the Editor. """ 71 | return self._editor_ref() 72 | 73 | @property 74 | def has_unsaved_changes(self): 75 | """ 76 | True when some changes are not yet written to file. 77 | """ 78 | return self._file_content != self.buffer.text 79 | 80 | @property 81 | def in_file_explorer_mode(self): 82 | """ 83 | True when we are in file explorer mode (when this is a directory). 84 | """ 85 | return self.isdir 86 | 87 | def _read(self, location): 88 | """ 89 | Read file I/O backend. 90 | """ 91 | for io in self.editor.io_backends: 92 | if io.can_open_location(location): 93 | # Found an I/O backend. 94 | exists = io.exists(location) 95 | self.isdir = io.isdir(location) 96 | 97 | if exists in (True, NotImplemented): 98 | # File could exist. Read it. 99 | self.is_new = False 100 | try: 101 | text, self.encoding = io.read(location) 102 | 103 | # Replace \r\n by \n. 104 | text = text.replace('\r\n', '\n') 105 | 106 | # Drop trailing newline while editing. 107 | # (prompt-toolkit doesn't enforce the trailing newline.) 108 | if text.endswith('\n'): 109 | text = text[:-1] 110 | except Exception as e: 111 | self.editor.show_message('Cannot read %r: %r' % (location, e)) 112 | return '' 113 | else: 114 | return text 115 | else: 116 | # File doesn't exist. 117 | self.is_new = True 118 | return '' 119 | 120 | self.editor.show_message('Cannot read: %r' % location) 121 | return '' 122 | 123 | def reload(self): 124 | """ 125 | Reload file again from storage. 126 | """ 127 | text = self._read(self.location) 128 | cursor_position = min(self.buffer.cursor_position, len(text)) 129 | 130 | self.buffer.document = Document(text, cursor_position) 131 | self._file_content = text 132 | 133 | def write(self, location=None): 134 | """ 135 | Write file to I/O backend. 136 | """ 137 | # Take location and expand tilde. 138 | if location is not None: 139 | self.location = location 140 | assert self.location 141 | 142 | # Find I/O backend that handles this location. 143 | for io in self.editor.io_backends: 144 | if io.can_open_location(self.location): 145 | break 146 | else: 147 | self.editor.show_message('Unknown location: %r' % location) 148 | 149 | # Write it. 150 | try: 151 | io.write(self.location, self.buffer.text + '\n', self.encoding) 152 | self.is_new = False 153 | except Exception as e: 154 | # E.g. "No such file or directory." 155 | self.editor.show_message('%s' % e) 156 | else: 157 | # When the save succeeds: update: _file_content. 158 | self._file_content = self.buffer.text 159 | 160 | def get_display_name(self, short=False): 161 | """ 162 | Return name as displayed. 163 | """ 164 | if self.location is None: 165 | return '[New file]' 166 | elif short: 167 | return os.path.basename(self.location) 168 | else: 169 | return self.location 170 | 171 | def __repr__(self): 172 | return '%s(buffer=%r)' % (self.__class__.__name__, self.buffer) 173 | 174 | def run_reporter(self): 175 | " Buffer text changed. " 176 | if not self._reporter_is_running: 177 | self._reporter_is_running = True 178 | 179 | text = self.buffer.text 180 | self.report_errors = [] 181 | 182 | # Don't run reporter when we don't have a location. (We need to 183 | # know the filetype, actually.) 184 | if self.location is None: 185 | return 186 | 187 | # Better not to access the document in an executor. 188 | document = self.buffer.document 189 | 190 | if PTK3: 191 | loop = get_event_loop() 192 | 193 | def in_executor(): 194 | # Call reporter 195 | report_errors = report(self.location, document) 196 | 197 | def ready(): 198 | self._reporter_is_running = False 199 | 200 | # If the text has not been changed yet in the meantime, set 201 | # reporter errors. (We were running in another thread.) 202 | if text == self.buffer.text: 203 | self.report_errors = report_errors 204 | get_app().invalidate() 205 | else: 206 | # Restart reporter when the text was changed. 207 | self.run_reporter() 208 | 209 | if PTK3: 210 | loop.call_soon_threadsafe(ready) 211 | else: 212 | call_from_executor(ready) 213 | 214 | if PTK3: 215 | loop.run_in_executor(None, in_executor) 216 | else: 217 | run_in_executor(in_executor) 218 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | pyvim 2 | ===== 3 | 4 | *An implementation of Vim in Python* 5 | 6 | :: 7 | 8 | pip install pyvim 9 | 10 | .. image :: https://github.com/jonathanslenders/pyvim/raw/master/docs/images/welcome-screen.png 11 | 12 | Issues, questions, wishes, comments, feedback, remarks? Please create a GitHub 13 | issue, I appreciate it. 14 | 15 | |Build Status| 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | Simply install ``pyvim`` using pip: 22 | 23 | :: 24 | 25 | pip install pyvim 26 | 27 | 28 | It is a good idea to add the following to your ``~/.bashrc`` if you really 29 | want to use it: 30 | 31 | :: 32 | 33 | alias vi=pyvim 34 | export EDITOR=pyvim 35 | 36 | 37 | The good things 38 | --------------- 39 | 40 | The editor is written completely in Python. (There are no C extensions). This 41 | makes development a lot faster. It's easy to prototype and integrate new 42 | features. 43 | 44 | We have already many nice things, for instance: 45 | 46 | - Syntax highlighting of files, using the Pygments lexers. 47 | 48 | - Horizontal and vertical splits, as well as tab pages. (Similar to Vim.) 49 | 50 | - All of the functionality of `prompt_toolkit 51 | `_. This includes a 52 | lot of Vi key bindings, it's platform independent and runs on every Python 53 | version from python 2.6 up to 3.4. It also runs on Pypy with a noticeable 54 | performance boost. 55 | 56 | - Several ``:set ...`` commands have been implemented, like ``incsearch``, 57 | ``number``, ``ignorecase``, ``wildmenu``, ``expandtab``, ``hlsearch``, 58 | ``ruler``, ``paste`` and ``tabstop``. 59 | 60 | - Other working commands: ``vsplit``, ``tabnew``, ``only``, ``badd``, and many 61 | others. 62 | 63 | - For Python source code, auto completion uses the amazing Jedi library, and 64 | code checking in done (asynchronously) through Pyflakes. 65 | 66 | - Colorschemes can be changed at runtime. 67 | 68 | Further, when the project develops, it should also become possible to write 69 | extensions in Python, and use Python as a scripting language. (Instead of 70 | vimscript, for instance.) 71 | 72 | We can also do some cool stuff. Like for instance running the editor on the 73 | Python asyncio event loop and having other coroutines interact with the editor. 74 | 75 | 76 | Some more screenshots 77 | --------------------- 78 | 79 | Editing its own source code: 80 | 81 | .. image :: https://github.com/jonathanslenders/pyvim/raw/master/docs/images/editing-pyvim-source.png 82 | 83 | Window layouts (horizontal and vertical splits + tab pages.) 84 | 85 | .. image :: https://github.com/jonathanslenders/pyvim/raw/master/docs/images/window-layout.png 86 | 87 | Pyflakes for Python code checking and Jedi for autocompletion: 88 | 89 | .. image :: https://github.com/jonathanslenders/pyvim/raw/master/docs/images/pyflakes-and-jedi.png 90 | 91 | Other colorschemes: 92 | 93 | .. image :: https://github.com/jonathanslenders/pyvim/raw/master/docs/images/colorschemes.png 94 | 95 | Chinese and Japanese input (double width characters): 96 | 97 | .. image :: https://raw.githubusercontent.com/jonathanslenders/pyvim/master/docs/images/cjk.png?v2 98 | 99 | 100 | Configuring pyvim 101 | ----------------- 102 | 103 | It is possible to create a ``.pyvimrc`` file for a custom configuration. 104 | Have a look at this example: `pyvimrc 105 | `_ 106 | 107 | 108 | Limitations 109 | ----------- 110 | 111 | Compared to Vi Improved, Pyvim is still less powerful in many aspects. 112 | 113 | - ``prompt_toolkit`` does not (or not yet) allow buffers to have an individual 114 | cursor when buffers are opened in several windows. Currently, this results in 115 | some unexpected behaviour, when a file is displayed in two windows at the 116 | same time. (The cursor could be displayed in the wrong window and other 117 | windows will sometimes scroll along when the cursor moves.) This has to be 118 | fixed in the future. 119 | - The data structure for a buffer is extremely simple. (Right now, it's just a 120 | Python string, and an integer for the cursor position.) This works extremely 121 | well for development and quickly prototyping of new features, but it comes 122 | with a performance penalty. Depending on the system, when a file has above a 123 | thousand lines and syntax highlighting is enabled, editing will become 124 | noticeable slower. (The bottleneck is probably the ``BufferControl`` code, 125 | which on every key press tries to reflow the text and calls pygments for 126 | highlighting. And this is Python code looping through single characters.) 127 | - A lot of nice Vim features, like line folding, macros, etcetera are not yet 128 | implemented. 129 | - Windows support is not that nice. It works, but could be improved. (I think 130 | most Windows users are not that interested in this project, but prove me 131 | wrong.) 132 | 133 | 134 | Roadmap 135 | ------- 136 | 137 | There is no roadmap. I mostly implement the stuff which I need or interests me, 138 | or which gives me the opportunity to learn. But feel free to create a GitHub 139 | issue to request a new feature. Pull requests are also welcome. (Maybe create 140 | an issue first to discuss it, if you're unsure whether I'll merge it.) 141 | 142 | Maybe some day we will have a better data structure (Rope), that makes it 143 | possible to open really large files. (With good algorithms, Python does not have 144 | to be slower than C code.) 145 | 146 | Maybe we will also have line folding and probably block editing. Maybe some 147 | day we will have a built-in Python debugger or mouse support. We'll see. :) 148 | 149 | 150 | Testing 151 | ------- 152 | 153 | To run all tests, install pytest: 154 | 155 | pip install pytest 156 | 157 | And then run from root pyvim directory: 158 | 159 | py.test 160 | 161 | To test pyvim against all supported python versions, install tox: 162 | 163 | pip install tox 164 | 165 | And then run from root pyvim directory: 166 | 167 | tox 168 | 169 | You need to have installed all the supported versions of python in order to run 170 | tox command successfully. 171 | 172 | 173 | Why did I create Pyvim? 174 | ----------------------- 175 | 176 | There are several reasons. 177 | 178 | The main reason is maybe because it was a small step after I created the Python 179 | ``prompt-toolkit`` library. That is a library which is actually only a simply 180 | pure Python readline replacement, but with some nice additions like syntax 181 | highlighting and multiline editing. It was never intended to be a toolkit for 182 | full-screen terminal applications, but at some point I realised that everything 183 | we need for an editor was in there and I liked to challenge its design. So, I 184 | started an editor and the first proof of concept was literally just a few 185 | hundred lines of code, but it was already a working editor. 186 | 187 | The creation of ``pyvim`` will make sure that we have a solid architecture for 188 | ``prompt-toolkit``, but it also aims to demonstrate the flexibility of the 189 | library. When it makes sense, features of ``pyvim`` will move back to 190 | ``prompt-toolkit``, which in turn also results in a better Python REPL. 191 | (see `ptpython `_, an alternative 192 | REPL.) 193 | 194 | Above all, it is really fun to create an editor. 195 | 196 | 197 | Alternatives 198 | ------------ 199 | 200 | Certainly have a look at the alternatives: 201 | 202 | - Kaa: https://github.com/kaaedit/kaa by @atsuoishimoto 203 | - Vai: https://github.com/stefanoborini/vai by @stefanoborini 204 | - Vis: https://github.com/martanne/vis by @martanne 205 | 206 | 207 | Q & A: 208 | ------ 209 | 210 | Q 211 | Do you use curses? 212 | A 213 | No, it uses only ``prompt-toolkit``. 214 | 215 | 216 | Thanks 217 | ------ 218 | 219 | - To Vi Improved, by Bram Moolenaar. For the inspiration. 220 | - To Jedi, pyflakes and the docopt Python libraries. 221 | - To the Python wcwidth port of Jeff Quast for support of double width characters. 222 | - To Guido van Rossum, for creating Python. 223 | 224 | 225 | .. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/pyvim.svg?branch=master 226 | :target: https://travis-ci.org/jonathanslenders/pyvim# 227 | -------------------------------------------------------------------------------- /pyvim/editor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The main editor class. 3 | 4 | Usage:: 5 | 6 | files_to_edit = ['file1.txt', 'file2.py'] 7 | e = Editor(files_to_edit) 8 | e.run() # Runs the event loop, starts interaction. 9 | """ 10 | from __future__ import unicode_literals 11 | 12 | from prompt_toolkit.application import Application 13 | from prompt_toolkit.buffer import Buffer 14 | from prompt_toolkit.enums import EditingMode 15 | from prompt_toolkit.filters import Condition 16 | from prompt_toolkit.history import FileHistory 17 | from prompt_toolkit.key_binding.vi_state import InputMode 18 | from prompt_toolkit.styles import DynamicStyle 19 | 20 | from .commands.completer import create_command_completer 21 | from .commands.handler import handle_command 22 | from .commands.preview import CommandPreviewer 23 | from .help import HELP_TEXT 24 | from .key_bindings import create_key_bindings 25 | from .layout import EditorLayout, get_terminal_title 26 | from .style import generate_built_in_styles, get_editor_style_by_name 27 | from .window_arrangement import WindowArrangement 28 | from .io import FileIO, DirectoryIO, HttpIO, GZipFileIO 29 | 30 | import pygments 31 | import os 32 | 33 | __all__ = ( 34 | 'Editor', 35 | ) 36 | 37 | 38 | class Editor(object): 39 | """ 40 | The main class. Containing the whole editor. 41 | 42 | :param config_directory: Place where configuration is stored. 43 | :param input: (Optionally) `prompt_toolkit.input.Input` object. 44 | :param output: (Optionally) `prompt_toolkit.output.Output` object. 45 | """ 46 | def __init__(self, config_directory='~/.pyvim', input=None, output=None): 47 | self.input = input 48 | self.output = output 49 | 50 | # Vi options. 51 | self.show_line_numbers = True 52 | self.highlight_search = True 53 | self.paste_mode = False 54 | self.show_ruler = True 55 | self.show_wildmenu = True 56 | self.expand_tab = True # Insect spaces instead of tab characters. 57 | self.tabstop = 4 # Number of spaces that a tab character represents. 58 | self.incsearch = True # Show matches while typing search string. 59 | self.ignore_case = False # Ignore case while searching. 60 | self.enable_mouse_support = True 61 | self.display_unprintable_characters = True # ':set list' 62 | self.enable_jedi = True # ':set jedi', for Python Jedi completion. 63 | self.scroll_offset = 0 # ':set scrolloff' 64 | self.relative_number = False # ':set relativenumber' 65 | self.wrap_lines = True # ':set wrap' 66 | self.break_indent = False # ':set breakindent' 67 | self.cursorline = False # ':set cursorline' 68 | self.cursorcolumn = False # ':set cursorcolumn' 69 | self.colorcolumn = [] # ':set colorcolumn'. List of integers. 70 | 71 | # Ensure config directory exists. 72 | self.config_directory = os.path.abspath(os.path.expanduser(config_directory)) 73 | if not os.path.exists(self.config_directory): 74 | os.mkdir(self.config_directory) 75 | 76 | self.window_arrangement = WindowArrangement(self) 77 | self.message = None 78 | 79 | # Load styles. (Mapping from name to Style class.) 80 | self.styles = generate_built_in_styles() 81 | self.current_style = get_editor_style_by_name('vim') 82 | 83 | # I/O backends. 84 | self.io_backends = [ 85 | DirectoryIO(), 86 | HttpIO(), 87 | GZipFileIO(), # Should come before FileIO. 88 | FileIO(), 89 | ] 90 | 91 | # Create history and search buffers. 92 | def handle_action(buff): 93 | ' When enter is pressed in the Vi command line. ' 94 | text = buff.text # Remember: leave_command_mode resets the buffer. 95 | 96 | # First leave command mode. We want to make sure that the working 97 | # pane is focussed again before executing the command handlers. 98 | self.leave_command_mode(append_to_history=True) 99 | 100 | # Execute command. 101 | handle_command(self, text) 102 | 103 | commands_history = FileHistory(os.path.join(self.config_directory, 'commands_history')) 104 | self.command_buffer = Buffer( 105 | accept_handler=handle_action, 106 | enable_history_search=True, 107 | completer=create_command_completer(self), 108 | history=commands_history, 109 | multiline=False) 110 | 111 | search_buffer_history = FileHistory(os.path.join(self.config_directory, 'search_history')) 112 | self.search_buffer = Buffer( 113 | history=search_buffer_history, 114 | enable_history_search=True, 115 | multiline=False) 116 | 117 | # Create key bindings registry. 118 | self.key_bindings = create_key_bindings(self) 119 | 120 | # Create layout and CommandLineInterface instance. 121 | self.editor_layout = EditorLayout(self, self.window_arrangement) 122 | self.application = self._create_application() 123 | 124 | # Hide message when a key is pressed. 125 | def key_pressed(_): 126 | self.message = None 127 | self.application.key_processor.before_key_press += key_pressed 128 | 129 | # Command line previewer. 130 | self.previewer = CommandPreviewer(self) 131 | 132 | self.last_substitute_text = '' 133 | 134 | def load_initial_files(self, locations, in_tab_pages=False, hsplit=False, vsplit=False): 135 | """ 136 | Load a list of files. 137 | """ 138 | assert in_tab_pages + hsplit + vsplit <= 1 # Max one of these options. 139 | 140 | # When no files were given, open at least one empty buffer. 141 | locations2 = locations or [None] 142 | 143 | # First file 144 | self.window_arrangement.open_buffer(locations2[0]) 145 | 146 | for f in locations2[1:]: 147 | if in_tab_pages: 148 | self.window_arrangement.create_tab(f) 149 | elif hsplit: 150 | self.window_arrangement.hsplit(location=f) 151 | elif vsplit: 152 | self.window_arrangement.vsplit(location=f) 153 | else: 154 | self.window_arrangement.open_buffer(f) 155 | 156 | self.window_arrangement.active_tab_index = 0 157 | 158 | if locations and len(locations) > 1: 159 | self.show_message('%i files loaded.' % len(locations)) 160 | 161 | def _create_application(self): 162 | """ 163 | Create CommandLineInterface instance. 164 | """ 165 | # Create Application. 166 | application = Application( 167 | input=self.input, 168 | output=self.output, 169 | editing_mode=EditingMode.VI, 170 | layout=self.editor_layout.layout, 171 | key_bindings=self.key_bindings, 172 | # get_title=lambda: get_terminal_title(self), 173 | style=DynamicStyle(lambda: self.current_style), 174 | paste_mode=Condition(lambda: self.paste_mode), 175 | # ignore_case=Condition(lambda: self.ignore_case), # TODO 176 | include_default_pygments_style=False, 177 | mouse_support=Condition(lambda: self.enable_mouse_support), 178 | full_screen=True, 179 | enable_page_navigation_bindings=True) 180 | 181 | # Handle command line previews. 182 | # (e.g. when typing ':colorscheme blue', it should already show the 183 | # preview before pressing enter.) 184 | def preview(_): 185 | if self.application.layout.has_focus(self.command_buffer): 186 | self.previewer.preview(self.command_buffer.text) 187 | self.command_buffer.on_text_changed += preview 188 | 189 | return application 190 | 191 | @property 192 | def current_editor_buffer(self): 193 | """ 194 | Return the `EditorBuffer` that is currently active. 195 | """ 196 | current_buffer = self.application.current_buffer 197 | 198 | # Find/return the EditorBuffer with this name. 199 | for b in self.window_arrangement.editor_buffers: 200 | if b.buffer == current_buffer: 201 | return b 202 | 203 | @property 204 | def add_key_binding(self): 205 | """ 206 | Shortcut for adding new key bindings. 207 | (Mostly useful for a pyvimrc file, that receives this Editor instance 208 | as input.) 209 | """ 210 | return self.key_bindings.add 211 | 212 | def show_message(self, message): 213 | """ 214 | Set a warning message. The layout will render it as a "pop-up" at the 215 | bottom. 216 | """ 217 | self.message = message 218 | 219 | def use_colorscheme(self, name='default'): 220 | """ 221 | Apply new colorscheme. (By name.) 222 | """ 223 | try: 224 | self.current_style = get_editor_style_by_name(name) 225 | except pygments.util.ClassNotFound: 226 | pass 227 | 228 | def sync_with_prompt_toolkit(self): 229 | """ 230 | Update the prompt-toolkit Layout and FocusStack. 231 | """ 232 | # After executing a command, make sure that the layout of 233 | # prompt-toolkit matches our WindowArrangement. 234 | self.editor_layout.update() 235 | 236 | # Make sure that the focus stack of prompt-toolkit has the current 237 | # page. 238 | window = self.window_arrangement.active_pt_window 239 | if window: 240 | self.application.layout.focus(window) 241 | 242 | def show_help(self): 243 | """ 244 | Show help in new window. 245 | """ 246 | self.window_arrangement.hsplit(text=HELP_TEXT) 247 | self.sync_with_prompt_toolkit() # Show new window. 248 | 249 | def run(self): 250 | """ 251 | Run the event loop for the interface. 252 | This starts the interaction. 253 | """ 254 | # Make sure everything is in sync, before starting. 255 | self.sync_with_prompt_toolkit() 256 | 257 | def pre_run(): 258 | # Start in navigation mode. 259 | self.application.vi_state.input_mode = InputMode.NAVIGATION 260 | 261 | # Run eventloop of prompt_toolkit. 262 | self.application.run(pre_run=pre_run) 263 | 264 | def enter_command_mode(self): 265 | """ 266 | Go into command mode. 267 | """ 268 | self.application.layout.focus(self.command_buffer) 269 | self.application.vi_state.input_mode = InputMode.INSERT 270 | 271 | self.previewer.save() 272 | 273 | def leave_command_mode(self, append_to_history=False): 274 | """ 275 | Leave command mode. Focus document window again. 276 | """ 277 | self.previewer.restore() 278 | 279 | self.application.layout.focus_last() 280 | self.application.vi_state.input_mode = InputMode.NAVIGATION 281 | 282 | self.command_buffer.reset(append_to_history=append_to_history) 283 | -------------------------------------------------------------------------------- /pyvim/window_arrangement.py: -------------------------------------------------------------------------------- 1 | """ 2 | Window arrangement. 3 | 4 | This contains the data structure for the tab pages with their windows and 5 | buffers. It's not the same as a `prompt-toolkit` layout. The latter directly 6 | represents the rendering, while this is more specific for the editor itself. 7 | """ 8 | from __future__ import unicode_literals 9 | from six import string_types 10 | import weakref 11 | 12 | from .editor_buffer import EditorBuffer 13 | 14 | __all__ = ( 15 | 'WindowArrangement', 16 | ) 17 | 18 | 19 | class HSplit(list): 20 | """ Horizontal split. (This is a higher level split than 21 | prompt_toolkit.layout.HSplit.) """ 22 | 23 | 24 | class VSplit(list): 25 | """ Horizontal split. """ 26 | 27 | 28 | class Window(object): 29 | """ 30 | Editor window: a window can show any open buffer. 31 | """ 32 | def __init__(self, editor_buffer): 33 | assert isinstance(editor_buffer, EditorBuffer) 34 | self.editor_buffer = editor_buffer 35 | 36 | # The prompt_toolkit layout Window. 37 | self.pt_window = None 38 | 39 | def __repr__(self): 40 | return '%s(editor_buffer=%r)' % (self.__class__.__name__, self.editor_buffer) 41 | 42 | 43 | class TabPage(object): 44 | """ 45 | Tab page. Container for windows. 46 | """ 47 | def __init__(self, window): 48 | assert isinstance(window, Window) 49 | self.root = VSplit([window]) 50 | 51 | # Keep track of which window is focusesd in this tab. 52 | self.active_window = window 53 | 54 | def windows(self): 55 | """ Return a list of all windows in this tab page. """ 56 | return [window for _, window in self._walk_through_windows()] 57 | 58 | def window_count(self): 59 | """ The amount of windows in this tab. """ 60 | return len(self.windows()) 61 | 62 | def visible_editor_buffers(self): 63 | """ 64 | Return a list of visible `EditorBuffer` instances. 65 | """ 66 | return [w.editor_buffer for w in self.windows()] 67 | 68 | def _walk_through_windows(self): 69 | """ 70 | Yields (Split, Window) tuples. 71 | """ 72 | def walk(split): 73 | for c in split: 74 | if isinstance(c, (HSplit, VSplit)): 75 | for i in walk(c): 76 | yield i 77 | elif isinstance(c, Window): 78 | yield split, c 79 | 80 | return walk(self.root) 81 | 82 | def _walk_through_splits(self): 83 | """ 84 | Yields (parent_split, child_plit) tuples. 85 | """ 86 | def walk(split): 87 | for c in split: 88 | if isinstance(c, (HSplit, VSplit)): 89 | yield split, c 90 | for i in walk(c): 91 | yield i 92 | 93 | return walk(self.root) 94 | 95 | def _get_active_split(self): 96 | for split, window in self._walk_through_windows(): 97 | if window == self.active_window: 98 | return split 99 | raise Exception('active_window not found. Something is wrong.') 100 | 101 | def _get_split_parent(self, split): 102 | for parent, child in self._walk_through_splits(): 103 | if child == split: 104 | return parent 105 | 106 | def _split(self, split_cls, editor_buffer=None): 107 | """ 108 | Split horizontal or vertical. 109 | (when editor_buffer is None, show the current buffer there as well.) 110 | """ 111 | if editor_buffer is None: 112 | editor_buffer = self.active_window.editor_buffer 113 | 114 | active_split = self._get_active_split() 115 | index = active_split.index(self.active_window) 116 | new_window = Window(editor_buffer) 117 | 118 | if isinstance(active_split, split_cls): 119 | # Add new window to active split. 120 | active_split.insert(index, new_window) 121 | else: 122 | # Split in the other direction. 123 | active_split[index] = split_cls([active_split[index], new_window]) 124 | 125 | # Focus new window. 126 | self.active_window = new_window 127 | 128 | def hsplit(self, editor_buffer=None): 129 | """ 130 | Split active window horizontally. 131 | """ 132 | self._split(HSplit, editor_buffer) 133 | 134 | def vsplit(self, editor_buffer=None): 135 | """ 136 | Split active window vertically. 137 | """ 138 | self._split(VSplit, editor_buffer) 139 | 140 | def show_editor_buffer(self, editor_buffer): 141 | """ 142 | Open this `EditorBuffer` in the active window. 143 | """ 144 | assert isinstance(editor_buffer, EditorBuffer) 145 | self.active_window.editor_buffer = editor_buffer 146 | 147 | def close_editor_buffer(self, editor_buffer): 148 | """ 149 | Close all the windows that have this editor buffer open. 150 | """ 151 | for split, window in self._walk_through_windows(): 152 | if window.editor_buffer == editor_buffer: 153 | self._close_window(window) 154 | 155 | def _close_window(self, window): 156 | """ 157 | Close this window. 158 | """ 159 | if window == self.active_window: 160 | self.close_active_window() 161 | else: 162 | original_active_window = self.active_window 163 | self.close_active_window() 164 | self.active_window = original_active_window 165 | 166 | def close_active_window(self): 167 | """ 168 | Close active window. 169 | """ 170 | active_split = self._get_active_split() 171 | 172 | # First remove the active window from its split. 173 | index = active_split.index(self.active_window) 174 | del active_split[index] 175 | 176 | # Move focus. 177 | if len(active_split): 178 | new_active_window = active_split[max(0, index - 1)] 179 | while isinstance(new_active_window, (HSplit, VSplit)): 180 | new_active_window = new_active_window[0] 181 | self.active_window = new_active_window 182 | else: 183 | self.active_window = None # No windows left. 184 | 185 | # When there is exactly on item left, move this back into the parent 186 | # split. (We don't want to keep a split with one item around -- except 187 | # for the root.) 188 | if len(active_split) == 1 and active_split != self.root: 189 | parent = self._get_split_parent(active_split) 190 | index = parent.index(active_split) 191 | parent[index] = active_split[0] 192 | 193 | def cycle_focus(self): 194 | """ 195 | Cycle through all windows. 196 | """ 197 | windows = self.windows() 198 | new_index = (windows.index(self.active_window) + 1) % len(windows) 199 | self.active_window = windows[new_index] 200 | 201 | @property 202 | def has_unsaved_changes(self): 203 | """ 204 | True when any of the visible buffers in this tab has unsaved changes. 205 | """ 206 | for w in self.windows(): 207 | if w.editor_buffer.has_unsaved_changes: 208 | return True 209 | return False 210 | 211 | 212 | class WindowArrangement(object): 213 | def __init__(self, editor): 214 | self._editor_ref = weakref.ref(editor) 215 | 216 | self.tab_pages = [] 217 | self.active_tab_index = None 218 | self.editor_buffers = [] # List of EditorBuffer 219 | 220 | @property 221 | def editor(self): 222 | """ The Editor instance. """ 223 | return self._editor_ref() 224 | 225 | @property 226 | def active_tab(self): 227 | """ The active TabPage or None. """ 228 | if self.active_tab_index is not None: 229 | return self.tab_pages[self.active_tab_index] 230 | 231 | @property 232 | def active_editor_buffer(self): 233 | """ The active EditorBuffer or None. """ 234 | if self.active_tab and self.active_tab.active_window: 235 | return self.active_tab.active_window.editor_buffer 236 | 237 | @property 238 | def active_pt_window(self): 239 | " The active prompt_toolkit layout Window. " 240 | if self.active_tab: 241 | w = self.active_tab.active_window 242 | if w: 243 | return w.pt_window 244 | 245 | def get_editor_buffer_for_location(self, location): 246 | """ 247 | Return the `EditorBuffer` for this location. 248 | When this file was not yet loaded, return None 249 | """ 250 | for eb in self.editor_buffers: 251 | if eb.location == location: 252 | return eb 253 | 254 | def get_editor_buffer_for_buffer_name(self, buffer_name): 255 | """ 256 | Return the `EditorBuffer` for this buffer_name. 257 | When not found, return None 258 | """ 259 | for eb in self.editor_buffers: 260 | if eb.buffer_name == buffer_name: 261 | return eb 262 | 263 | def close_window(self): 264 | """ 265 | Close active window of active tab. 266 | """ 267 | self.active_tab.close_active_window() 268 | 269 | # Clean up buffers. 270 | self._auto_close_new_empty_buffers() 271 | 272 | def close_tab(self): 273 | """ 274 | Close active tab. 275 | """ 276 | if len(self.tab_pages) > 1: # Cannot close last tab. 277 | del self.tab_pages[self.active_tab_index] 278 | self.active_tab_index = max(0, self.active_tab_index - 1) 279 | 280 | # Clean up buffers. 281 | self._auto_close_new_empty_buffers() 282 | 283 | def hsplit(self, location=None, new=False, text=None): 284 | """ Split horizontally. """ 285 | assert location is None or text is None or new is False # Don't pass two of them. 286 | 287 | if location or text or new: 288 | editor_buffer = self._get_or_create_editor_buffer(location=location, text=text) 289 | else: 290 | editor_buffer = None 291 | self.active_tab.hsplit(editor_buffer) 292 | 293 | def vsplit(self, location=None, new=False, text=None): 294 | """ Split vertically. """ 295 | assert location is None or text is None or new is False # Don't pass two of them. 296 | 297 | if location or text or new: 298 | editor_buffer = self._get_or_create_editor_buffer(location=location, text=text) 299 | else: 300 | editor_buffer = None 301 | self.active_tab.vsplit(editor_buffer) 302 | 303 | def keep_only_current_window(self): 304 | """ 305 | Close all other windows, except the current one. 306 | """ 307 | self.tab_pages = [TabPage(self.active_tab.active_window)] 308 | self.active_tab_index = 0 309 | 310 | def cycle_focus(self): 311 | """ Focus next visible window. """ 312 | self.active_tab.cycle_focus() 313 | 314 | def show_editor_buffer(self, editor_buffer): 315 | """ 316 | Show this EditorBuffer in the current window. 317 | """ 318 | self.active_tab.show_editor_buffer(editor_buffer) 319 | 320 | # Clean up buffers. 321 | self._auto_close_new_empty_buffers() 322 | 323 | def go_to_next_buffer(self, _previous=False): 324 | """ 325 | Open next buffer in active window. 326 | """ 327 | if self.active_editor_buffer: 328 | # Find the active opened buffer. 329 | index = self.editor_buffers.index(self.active_editor_buffer) 330 | 331 | # Get index of new buffer. 332 | if _previous: 333 | new_index = (len(self.editor_buffers) + index - 1) % len(self.editor_buffers) 334 | else: 335 | new_index = (index + 1) % len(self.editor_buffers) 336 | 337 | # Open new buffer in active tab. 338 | self.active_tab.show_editor_buffer(self.editor_buffers[new_index]) 339 | 340 | # Clean up buffers. 341 | self._auto_close_new_empty_buffers() 342 | 343 | def go_to_previous_buffer(self): 344 | """ 345 | Open the previous buffer in the active window. 346 | """ 347 | self.go_to_next_buffer(_previous=True) 348 | 349 | def go_to_next_tab(self): 350 | """ 351 | Focus the next tab. 352 | """ 353 | self.active_tab_index = (self.active_tab_index + 1) % len(self.tab_pages) 354 | 355 | def go_to_previous_tab(self): 356 | """ 357 | Focus the previous tab. 358 | """ 359 | self.active_tab_index = (self.active_tab_index - 1 + 360 | len(self.tab_pages)) % len(self.tab_pages) 361 | 362 | def go_to_buffer(self, buffer_name): 363 | """ 364 | Go to one of the open buffers. 365 | """ 366 | assert isinstance(buffer_name, string_types) 367 | 368 | for i, eb in enumerate(self.editor_buffers): 369 | if (eb.location == buffer_name or 370 | (buffer_name.isdigit() and int(buffer_name) == i)): 371 | self.show_editor_buffer(eb) 372 | break 373 | 374 | def _add_editor_buffer(self, editor_buffer, show_in_current_window=False): 375 | """ 376 | Insert this new buffer in the list of buffers, right after the active 377 | one. 378 | """ 379 | assert isinstance(editor_buffer, EditorBuffer) and editor_buffer not in self.editor_buffers 380 | 381 | # Add to list of EditorBuffers 382 | eb = self.active_editor_buffer 383 | if eb is None: 384 | self.editor_buffers.append(editor_buffer) 385 | else: 386 | # Append right after the currently active one. 387 | try: 388 | index = self.editor_buffers.index(self.active_editor_buffer) 389 | except ValueError: 390 | index = 0 391 | self.editor_buffers.insert(index, editor_buffer) 392 | 393 | # When there are no tabs/windows yet, create one for this buffer. 394 | if self.tab_pages == []: 395 | self.tab_pages.append(TabPage(Window(editor_buffer))) 396 | self.active_tab_index = 0 397 | 398 | # To be shown? 399 | if show_in_current_window and self.active_tab: 400 | self.active_tab.show_editor_buffer(editor_buffer) 401 | 402 | # Start reporter. 403 | editor_buffer.run_reporter() 404 | 405 | def _get_or_create_editor_buffer(self, location=None, text=None): 406 | """ 407 | Given a location, return the `EditorBuffer` instance that we have if 408 | the file is already open, or create a new one. 409 | 410 | When location is None, this creates a new buffer. 411 | """ 412 | assert location is None or text is None # Don't pass two of them. 413 | assert location is None or isinstance(location, string_types) 414 | 415 | if location is None: 416 | # Create and add an empty EditorBuffer 417 | eb = EditorBuffer(self.editor, text=text) 418 | self._add_editor_buffer(eb) 419 | 420 | return eb 421 | else: 422 | # When a location is given, first look whether the file was already 423 | # opened. 424 | eb = self.get_editor_buffer_for_location(location) 425 | 426 | # Not found? Create one. 427 | if eb is None: 428 | # Create and add EditorBuffer 429 | eb = EditorBuffer(self.editor, location) 430 | self._add_editor_buffer(eb) 431 | 432 | return eb 433 | else: 434 | # Found! Return it. 435 | return eb 436 | 437 | def open_buffer(self, location=None, show_in_current_window=False): 438 | """ 439 | Open/create a file, load it, and show it in a new buffer. 440 | """ 441 | eb = self._get_or_create_editor_buffer(location) 442 | 443 | if show_in_current_window: 444 | self.show_editor_buffer(eb) 445 | 446 | def _auto_close_new_empty_buffers(self): 447 | """ 448 | When there are new, empty buffers open. (Like, created when the editor 449 | starts without any files.) These can be removed at the point when there 450 | is no more window showing them. 451 | 452 | This should be called every time when a window is closed, or when the 453 | content of a window is replcaed by something new. 454 | """ 455 | # Get all visible EditorBuffers 456 | ebs = set() 457 | for t in self.tab_pages: 458 | ebs |= set(t.visible_editor_buffers()) 459 | 460 | # Remove empty/new buffers that are hidden. 461 | for eb in self.editor_buffers[:]: 462 | if eb.is_new and not eb.location and eb not in ebs and eb.buffer.text == '': 463 | self.editor_buffers.remove(eb) 464 | 465 | def close_buffer(self): 466 | """ 467 | Close current buffer. When there are other windows showing the same 468 | buffer, they are closed as well. When no windows are left, the previous 469 | buffer or an empty buffer is shown. 470 | """ 471 | eb = self.active_editor_buffer 472 | 473 | # Remove this buffer. 474 | index = self.editor_buffers.index(eb) 475 | self.editor_buffers.remove(eb) 476 | 477 | # Close the active window. 478 | self.active_tab.close_active_window() 479 | 480 | # Close all the windows that still have this buffer open. 481 | for i, t in enumerate(self.tab_pages[:]): 482 | t.close_editor_buffer(eb) 483 | 484 | # Remove tab when there are no windows left. 485 | if t.window_count() == 0: 486 | self.tab_pages.remove(t) 487 | 488 | if i >= self.active_tab_index: 489 | self.active_tab_index = max(0, self.active_tab_index - 1) 490 | 491 | # When there are no windows/tabs left, create a new tab. 492 | if len(self.tab_pages) == 0: 493 | self.active_tab_index = None 494 | 495 | if len(self.editor_buffers) > 0: 496 | # Open the previous buffer. 497 | new_index = (len(self.editor_buffers) + index - 1) % len(self.editor_buffers) 498 | eb = self.editor_buffers[new_index] 499 | 500 | # Create a window for this buffer. 501 | self.tab_pages.append(TabPage(Window(eb))) 502 | self.active_tab_index = 0 503 | else: 504 | # Create a new buffer. (This will also create the window 505 | # automatically.) 506 | eb = self._get_or_create_editor_buffer() 507 | 508 | def create_tab(self, location=None): 509 | """ 510 | Create a new tab page. 511 | """ 512 | eb = self._get_or_create_editor_buffer(location) 513 | 514 | self.tab_pages.insert(self.active_tab_index + 1, TabPage(Window(eb))) 515 | self.active_tab_index += 1 516 | 517 | def list_open_buffers(self): 518 | """ 519 | Return a `OpenBufferInfo` list that gives information about the 520 | open buffers. 521 | """ 522 | active_eb = self.active_editor_buffer 523 | visible_ebs = self.active_tab.visible_editor_buffers() 524 | 525 | def make_info(i, eb): 526 | return OpenBufferInfo( 527 | index=i, 528 | editor_buffer=eb, 529 | is_active=(eb == active_eb), 530 | is_visible=(eb in visible_ebs)) 531 | 532 | return [make_info(i, eb) for i, eb in enumerate(self.editor_buffers)] 533 | 534 | 535 | class OpenBufferInfo(object): 536 | """ 537 | Information about an open buffer, returned by 538 | `WindowArrangement.list_open_buffers`. 539 | """ 540 | def __init__(self, index, editor_buffer, is_active, is_visible): 541 | self.index = index 542 | self.editor_buffer = editor_buffer 543 | self.is_active = is_active 544 | self.is_visible = is_visible 545 | -------------------------------------------------------------------------------- /pyvim/commands/commands.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, print_function 2 | from prompt_toolkit.application import run_in_terminal 3 | from prompt_toolkit.document import Document 4 | 5 | import os 6 | import re 7 | import six 8 | 9 | __all__ = ( 10 | 'has_command_handler', 11 | 'call_command_handler', 12 | ) 13 | 14 | 15 | COMMANDS_TO_HANDLERS = {} # Global mapping Vi commands to their handler. 16 | COMMANDS_TAKING_LOCATIONS = set() # Name of commands that accept locations. 17 | SET_COMMANDS = {} # Mapping ':set'-commands to their handler. 18 | SET_COMMANDS_TAKING_VALUE = set() 19 | 20 | 21 | _NO_WRITE_SINCE_LAST_CHANGE_TEXT = 'No write since last change (add ! to override)' 22 | _NO_FILE_NAME = 'No file name' 23 | 24 | 25 | def has_command_handler(command): 26 | return command in COMMANDS_TO_HANDLERS 27 | 28 | 29 | def call_command_handler(command, editor, variables): 30 | """ 31 | Execute command. 32 | """ 33 | COMMANDS_TO_HANDLERS[command](editor, variables) 34 | 35 | 36 | def get_commands(): 37 | return COMMANDS_TO_HANDLERS.keys() 38 | 39 | 40 | def get_commands_taking_locations(): 41 | return COMMANDS_TAKING_LOCATIONS 42 | 43 | 44 | # Decorators 45 | 46 | def _cmd(name): 47 | """ 48 | Base decorator for registering commands in this namespace. 49 | """ 50 | def decorator(func): 51 | COMMANDS_TO_HANDLERS[name] = func 52 | return func 53 | return decorator 54 | 55 | 56 | def location_cmd(name, accepts_force=False): 57 | """ 58 | Decorator that registers a command that takes a location as (optional) 59 | parameter. 60 | """ 61 | COMMANDS_TAKING_LOCATIONS.add(name) 62 | 63 | def decorator(func): 64 | @_cmd(name) 65 | def command_wrapper(editor, variables): 66 | location = variables.get('location') 67 | force = bool(variables['force']) 68 | 69 | if force and not accepts_force: 70 | editor.show_message('No ! allowed') 71 | elif accepts_force: 72 | func(editor, location, force=force) 73 | else: 74 | func(editor, location) 75 | return func 76 | return decorator 77 | 78 | 79 | def cmd(name, accepts_force=False): 80 | """ 81 | Decarator that registers a command that doesn't take any parameters. 82 | """ 83 | def decorator(func): 84 | @_cmd(name) 85 | def command_wrapper(editor, variables): 86 | force = bool(variables['force']) 87 | 88 | if force and not accepts_force: 89 | editor.show_message('No ! allowed') 90 | elif accepts_force: 91 | func(editor, force=force) 92 | else: 93 | func(editor) 94 | return func 95 | return decorator 96 | 97 | 98 | def set_cmd(name, accepts_value=False): 99 | """ 100 | Docorator that registers a ':set'-command. 101 | """ 102 | def decorator(func): 103 | SET_COMMANDS[name] = func 104 | if accepts_value: 105 | SET_COMMANDS_TAKING_VALUE.add(name) 106 | return func 107 | return decorator 108 | 109 | 110 | # Actual command implementations 111 | 112 | @_cmd('set') 113 | def set_command_execute(editor, variables): 114 | """ 115 | Execute a ':set' command. 116 | """ 117 | option = variables.get('set_option') 118 | value = variables.get('set_value') 119 | 120 | if option in SET_COMMANDS: 121 | # Call the correct handler. 122 | if option in SET_COMMANDS_TAKING_VALUE: 123 | SET_COMMANDS[option](editor, value) 124 | else: 125 | SET_COMMANDS[option](editor) 126 | else: 127 | editor.show_message('Unknown option: %s' % option) 128 | 129 | 130 | @cmd('bn', accepts_force=True) 131 | def _bn(editor, force=False): 132 | """ 133 | Go to next buffer. 134 | """ 135 | eb = editor.window_arrangement.active_editor_buffer 136 | 137 | if not force and eb.has_unsaved_changes: 138 | editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) 139 | else: 140 | editor.window_arrangement.go_to_next_buffer() 141 | 142 | 143 | @cmd('bp', accepts_force=True) 144 | def _bp(editor, force=False): 145 | """ 146 | Go to previous buffer. 147 | """ 148 | eb = editor.window_arrangement.active_editor_buffer 149 | 150 | if not force and eb.has_unsaved_changes: 151 | editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) 152 | else: 153 | editor.window_arrangement.go_to_previous_buffer() 154 | 155 | 156 | @cmd('only') 157 | def only(editor): 158 | """ 159 | Keep only the current window. 160 | """ 161 | editor.window_arrangement.keep_only_current_window() 162 | 163 | 164 | @cmd('hide') 165 | def hide(editor): 166 | """ 167 | Hide the current window. 168 | """ 169 | editor.window_arrangement.close_window() 170 | 171 | 172 | @location_cmd('sp') 173 | @location_cmd('split') 174 | def horizontal_split(editor, location): 175 | """ 176 | Split window horizontally. 177 | """ 178 | editor.window_arrangement.hsplit(location or None) 179 | 180 | 181 | @location_cmd('vsp') 182 | @location_cmd('vsplit') 183 | def vertical_split(editor, location): 184 | """ 185 | Split window vertically. 186 | """ 187 | editor.window_arrangement.vsplit(location or None) 188 | 189 | 190 | @cmd('new') 191 | def new_buffer(editor): 192 | """ 193 | Create new buffer. 194 | """ 195 | editor.window_arrangement.hsplit(new=True) 196 | 197 | 198 | @cmd('vnew') 199 | def new_vertical_buffer(editor): 200 | """ 201 | Create new buffer, splitting vertically. 202 | """ 203 | editor.window_arrangement.vsplit(new=True) 204 | 205 | 206 | @location_cmd('badd') 207 | def buffer_add(editor, location): 208 | """ 209 | Add a new buffer. 210 | """ 211 | editor.window_arrangement.open_buffer(location) 212 | 213 | 214 | @cmd('buffers') 215 | def buffer_list(editor): 216 | """ 217 | List all buffers. 218 | """ 219 | def handler(): 220 | wa = editor.window_arrangement 221 | for info in wa.list_open_buffers(): 222 | char = '%' if info.is_active else '' 223 | eb = info.editor_buffer 224 | print(' %3i %-2s %-20s line %i' % ( 225 | info.index, char, eb.location, (eb.buffer.document.cursor_position_row + 1))) 226 | six.moves.input('\nPress ENTER to continue...') 227 | run_in_terminal(handler) 228 | 229 | 230 | @_cmd('b') 231 | @_cmd('buffer') 232 | def _buffer(editor, variables, force=False): 233 | """ 234 | Go to one of the open buffers. 235 | """ 236 | eb = editor.window_arrangement.active_editor_buffer 237 | force = bool(variables['force']) 238 | 239 | buffer_name = variables.get('buffer_name') 240 | if buffer_name: 241 | if not force and eb.has_unsaved_changes: 242 | editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) 243 | else: 244 | editor.window_arrangement.go_to_buffer(buffer_name) 245 | 246 | 247 | @cmd('bw', accepts_force=True) 248 | @cmd('bd', accepts_force=True) 249 | def buffer_wipe(editor, force=False): 250 | """ 251 | Wipe buffer. 252 | """ 253 | eb = editor.window_arrangement.active_editor_buffer 254 | if not force and eb.has_unsaved_changes: 255 | editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) 256 | else: 257 | editor.window_arrangement.close_buffer() 258 | 259 | 260 | @location_cmd('o', accepts_force=True) 261 | @location_cmd('open', accepts_force=True) 262 | @location_cmd('e', accepts_force=True) 263 | @location_cmd('edit', accepts_force=True) 264 | def buffer_edit(editor, location, force=False): 265 | """ 266 | Edit new buffer. 267 | """ 268 | if location is None: 269 | # Edit/open without a location will reload the current file, if there are 270 | # no changes. 271 | eb = editor.window_arrangement.active_editor_buffer 272 | if eb.location is None: 273 | editor.show_message(_NO_FILE_NAME) 274 | elif not force and eb.has_unsaved_changes: 275 | editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) 276 | else: 277 | eb.reload() 278 | else: 279 | editor.file_explorer = '' 280 | editor.window_arrangement.open_buffer(location, show_in_current_window=True) 281 | 282 | 283 | @cmd('q', accepts_force=True) 284 | @cmd('quit', accepts_force=True) 285 | def quit(editor, all_=False, force=False): 286 | """ 287 | Quit. 288 | """ 289 | ebs = editor.window_arrangement.editor_buffers 290 | 291 | # When there are buffers that have unsaved changes, show balloon. 292 | if not force and any(eb.has_unsaved_changes for eb in ebs): 293 | editor.show_message(_NO_WRITE_SINCE_LAST_CHANGE_TEXT) 294 | 295 | # When there is more than one buffer open. 296 | elif not all_ and len(ebs) > 1: 297 | editor.show_message('%i more files to edit' % (len(ebs) - 1)) 298 | 299 | else: 300 | editor.application.exit() 301 | 302 | 303 | @cmd('qa', accepts_force=True) 304 | @cmd('qall', accepts_force=True) 305 | def quit_all(editor, force=False): 306 | """ 307 | Quit all. 308 | """ 309 | quit(editor, all_=True, force=force) 310 | 311 | 312 | @location_cmd('w', accepts_force=True) 313 | @location_cmd('write', accepts_force=True) 314 | def write(editor, location, force=False): 315 | """ 316 | Write file. 317 | """ 318 | if location and not force and os.path.exists(location): 319 | editor.show_message('File exists (add ! to overriwe)') 320 | else: 321 | eb = editor.window_arrangement.active_editor_buffer 322 | if location is None and eb.location is None: 323 | editor.show_message(_NO_FILE_NAME) 324 | else: 325 | eb.write(location) 326 | 327 | 328 | @location_cmd('wq', accepts_force=True) 329 | def write_and_quit(editor, location, force=False): 330 | """ 331 | Write file and quit. 332 | """ 333 | write(editor, location, force=force) 334 | editor.application.exit() 335 | 336 | 337 | @cmd('cq') 338 | def quit_nonzero(editor): 339 | """ 340 | Quit with non zero exit status. 341 | """ 342 | # Note: the try/finally in `prompt_toolkit.Interface.read_input` 343 | # will ensure that the render output is reset, leaving the alternate 344 | # screen before quitting. 345 | editor.application.exit() 346 | 347 | 348 | @cmd('wqa') 349 | def write_and_quit_all(editor): 350 | """ 351 | Write current buffer and quit all. 352 | """ 353 | eb = editor.window_arrangement.active_editor_buffer 354 | if eb.location is None: 355 | editor.show_message(_NO_FILE_NAME) 356 | else: 357 | eb.write() 358 | quit(editor, all_=True, force=False) 359 | 360 | 361 | @cmd('h') 362 | @cmd('help') 363 | def help(editor): 364 | """ 365 | Show help. 366 | """ 367 | editor.show_help() 368 | 369 | 370 | @location_cmd('tabe') 371 | @location_cmd('tabedit') 372 | @location_cmd('tabnew') 373 | def tab_new(editor, location): 374 | """ 375 | Create new tab page. 376 | """ 377 | editor.window_arrangement.create_tab(location or None) 378 | 379 | 380 | @cmd('tabclose') 381 | @cmd('tabc') 382 | def tab_close(editor): 383 | """ 384 | Close tab page. 385 | """ 386 | editor.window_arrangement.close_tab() 387 | 388 | 389 | @cmd('tabnext') 390 | @cmd('tabn') 391 | def tab_next(editor): 392 | """ 393 | Go to next tab. 394 | """ 395 | editor.window_arrangement.go_to_next_tab() 396 | 397 | 398 | @cmd('tabprevious') 399 | @cmd('tabp') 400 | def tab_previous(editor): 401 | """ 402 | Go to previous tab. 403 | """ 404 | editor.window_arrangement.go_to_previous_tab() 405 | 406 | 407 | @cmd('pwd') 408 | def pwd(editor): 409 | " Print working directory. " 410 | directory = os.getcwd() 411 | editor.show_message('{}'.format(directory)) 412 | 413 | 414 | @location_cmd('cd', accepts_force=False) 415 | def pwd(editor, location): 416 | " Change working directory. " 417 | try: 418 | os.chdir(location) 419 | except OSError as e: 420 | editor.show_message('{}'.format(e)) 421 | 422 | 423 | @_cmd('colorscheme') 424 | @_cmd('colo') 425 | def color_scheme(editor, variables): 426 | """ 427 | Go to one of the open buffers. 428 | """ 429 | colorscheme = variables.get('colorscheme') 430 | if colorscheme: 431 | editor.use_colorscheme(colorscheme) 432 | 433 | 434 | @set_cmd('nu') 435 | @set_cmd('number') 436 | def line_numbers_show(editor): 437 | """ Show line numbers. """ 438 | editor.show_line_numbers = True 439 | 440 | 441 | @set_cmd('nonu') 442 | @set_cmd('nonumber') 443 | def line_numbers_hide(editor): 444 | """ Hide line numbers. """ 445 | editor.show_line_numbers = False 446 | 447 | 448 | @set_cmd('hlsearch') 449 | @set_cmd('hls') 450 | def search_highlight(editor): 451 | """ Highlight search matches. """ 452 | editor.highlight_search = True 453 | 454 | 455 | @set_cmd('nohlsearch') 456 | @set_cmd('nohls') 457 | def search_no_highlight(editor): 458 | """ Don't highlight search matches. """ 459 | editor.highlight_search = False 460 | 461 | 462 | @set_cmd('paste') 463 | def paste_mode(editor): 464 | """ Enter paste mode. """ 465 | editor.paste_mode = True 466 | 467 | 468 | @set_cmd('nopaste') 469 | def paste_mode_leave(editor): 470 | """ Leave paste mode. """ 471 | editor.paste_mode = False 472 | 473 | 474 | @set_cmd('ruler') 475 | @set_cmd('ru') 476 | def ruler_show(editor): 477 | """ Show ruler. """ 478 | editor.show_ruler = True 479 | 480 | 481 | @set_cmd('noruler') 482 | @set_cmd('noru') 483 | def ruler_hide(editor): 484 | """ Hide ruler. """ 485 | editor.show_ruler = False 486 | 487 | 488 | @set_cmd('wildmenu') 489 | @set_cmd('wmnu') 490 | def wild_menu_show(editor): 491 | """ Show wildmenu. """ 492 | editor.show_wildmenu = True 493 | 494 | 495 | @set_cmd('nowildmenu') 496 | @set_cmd('nowmnu') 497 | def wild_menu_hide(editor): 498 | """ Hide wildmenu. """ 499 | editor.show_wildmenu = False 500 | 501 | 502 | @set_cmd('expandtab') 503 | @set_cmd('et') 504 | def tab_expand(editor): 505 | """ Enable tab expension. """ 506 | editor.expand_tab = True 507 | 508 | 509 | @set_cmd('noexpandtab') 510 | @set_cmd('noet') 511 | def tab_no_expand(editor): 512 | """ Disable tab expension. """ 513 | editor.expand_tab = False 514 | 515 | 516 | @set_cmd('tabstop', accepts_value=True) 517 | @set_cmd('ts', accepts_value=True) 518 | def tab_stop(editor, value): 519 | """ 520 | Set tabstop. 521 | """ 522 | if value is None: 523 | editor.show_message('tabstop=%i' % editor.tabstop) 524 | else: 525 | try: 526 | value = int(value) 527 | if value > 0: 528 | editor.tabstop = value 529 | else: 530 | editor.show_message('Argument must be positive') 531 | except ValueError: 532 | editor.show_message('Number required after =') 533 | 534 | 535 | @set_cmd('scrolloff', accepts_value=True) 536 | @set_cmd('so', accepts_value=True) 537 | def set_scroll_offset(editor, value): 538 | """ 539 | Set scroll offset. 540 | """ 541 | if value is None: 542 | editor.show_message('scrolloff=%i' % editor.scroll_offset) 543 | else: 544 | try: 545 | value = int(value) 546 | if value >= 0: 547 | editor.scroll_offset = value 548 | else: 549 | editor.show_message('Argument must be positive') 550 | except ValueError: 551 | editor.show_message('Number required after =') 552 | 553 | 554 | @set_cmd('incsearch') 555 | @set_cmd('is') 556 | def incsearch_enable(editor): 557 | """ Enable incsearch. """ 558 | editor.incsearch = True 559 | 560 | 561 | @set_cmd('noincsearch') 562 | @set_cmd('nois') 563 | def incsearch_disable(editor): 564 | """ Disable incsearch. """ 565 | editor.incsearch = False 566 | 567 | 568 | @set_cmd('ignorecase') 569 | @set_cmd('ic') 570 | def search_ignorecase(editor): 571 | """ Enable case insensitive searching. """ 572 | editor.ignore_case = True 573 | 574 | 575 | @set_cmd('noignorecase') 576 | @set_cmd('noic') 577 | def searc_no_ignorecase(editor): 578 | """ Disable case insensitive searching. """ 579 | editor.ignore_case = False 580 | 581 | 582 | @set_cmd('list') 583 | def unprintable_show(editor): 584 | """ Display unprintable characters. """ 585 | editor.display_unprintable_characters = True 586 | 587 | 588 | @set_cmd('nolist') 589 | def unprintable_hide(editor): 590 | """ Hide unprintable characters. """ 591 | editor.display_unprintable_characters = False 592 | 593 | 594 | @set_cmd('jedi') 595 | def jedi_enable(editor): 596 | """ Enable Jedi autocompletion for Python files. """ 597 | editor.enable_jedi = True 598 | 599 | 600 | @set_cmd('nojedi') 601 | def jedi_disable(editor): 602 | """ Disable Jedi autocompletion. """ 603 | editor.enable_jedi = False 604 | 605 | 606 | @set_cmd('relativenumber') 607 | @set_cmd('rnu') 608 | def relative_number(editor): 609 | " Enable relative number " 610 | editor.relative_number = True 611 | 612 | 613 | @set_cmd('norelativenumber') 614 | @set_cmd('nornu') 615 | def no_relative_number(editor): 616 | " Disable relative number " 617 | editor.relative_number = False 618 | 619 | 620 | @set_cmd('wrap') 621 | def enable_wrap(editor): 622 | " Enable line wrapping. " 623 | editor.wrap_lines = True 624 | 625 | 626 | @set_cmd('nowrap') 627 | def disable_wrap(editor): 628 | " disable line wrapping. " 629 | editor.wrap_lines = False 630 | 631 | 632 | @set_cmd('breakindent') 633 | @set_cmd('bri') 634 | def enable_breakindent(editor): 635 | " Enable the breakindent option. " 636 | editor.break_indent = True 637 | 638 | 639 | @set_cmd('nobreakindent') 640 | @set_cmd('nobri') 641 | def disable_breakindent(editor): 642 | " Enable the breakindent option. " 643 | editor.break_indent = False 644 | 645 | 646 | @set_cmd('mouse') 647 | def enable_mouse(editor): 648 | " Enable mouse . " 649 | editor.enable_mouse_support = True 650 | 651 | 652 | @set_cmd('nomouse') 653 | def disable_mouse(editor): 654 | " Disable mouse. " 655 | editor.enable_mouse_support = False 656 | 657 | 658 | @set_cmd('tildeop') 659 | @set_cmd('top') 660 | def enable_tildeop(editor): 661 | " Enable tilde operator. " 662 | editor.application.vi_state.tilde_operator = True 663 | 664 | 665 | @set_cmd('notildeop') 666 | @set_cmd('notop') 667 | def disable_tildeop(editor): 668 | " Disable tilde operator. " 669 | editor.application.vi_state.tilde_operator = False 670 | 671 | 672 | @set_cmd('cursorline') 673 | @set_cmd('cul') 674 | def enable_cursorline(editor): 675 | " Highlight the line that contains the cursor. " 676 | editor.cursorline = True 677 | 678 | @set_cmd('nocursorline') 679 | @set_cmd('nocul') 680 | def disable_cursorline(editor): 681 | " No cursorline. " 682 | editor.cursorline = False 683 | 684 | @set_cmd('cursorcolumn') 685 | @set_cmd('cuc') 686 | def enable_cursorcolumn(editor): 687 | " Highlight the column that contains the cursor. " 688 | editor.cursorcolumn = True 689 | 690 | @set_cmd('nocursorcolumn') 691 | @set_cmd('nocuc') 692 | def disable_cursorcolumn(editor): 693 | " No cursorcolumn. " 694 | editor.cursorcolumn = False 695 | 696 | 697 | @set_cmd('colorcolumn', accepts_value=True) 698 | @set_cmd('cc', accepts_value=True) 699 | def set_scroll_offset(editor, value): 700 | try: 701 | if value: 702 | numbers = [int(val) for val in value.split(',')] 703 | else: 704 | numbers = [] 705 | except ValueError: 706 | editor.show_message( 707 | 'Invalid value. Expecting comma separated list of integers') 708 | else: 709 | editor.colorcolumn = numbers 710 | 711 | 712 | def substitute(editor, range_start, range_end, search, replace, flags): 713 | """ Substitute /search/ with /replace/ over a range of text """ 714 | def get_line_index_iterator(cursor_position_row, range_start, range_end): 715 | if not range_start: 716 | assert not range_end 717 | range_start = range_end = cursor_position_row 718 | else: 719 | range_start = int(range_start) - 1 720 | range_end = int(range_end) - 1 if range_end else range_start 721 | return range(range_start, range_end + 1) 722 | 723 | def get_transform_callback(search, replace, flags): 724 | SUBSTITUTE_ALL, SUBSTITUTE_ONE = 0, 1 725 | sub_count = SUBSTITUTE_ALL if 'g' in flags else SUBSTITUTE_ONE 726 | return lambda s: re.sub(search, replace, s, count=sub_count) 727 | 728 | search_state = editor.application.current_search_state 729 | buffer = editor.current_editor_buffer.buffer 730 | cursor_position_row = buffer.document.cursor_position_row 731 | 732 | # read editor state 733 | if not search: 734 | search = search_state.text 735 | 736 | if replace is None: 737 | replace = editor.last_substitute_text 738 | 739 | line_index_iterator = get_line_index_iterator(cursor_position_row, range_start, range_end) 740 | transform_callback = get_transform_callback(search, replace, flags) 741 | new_text = buffer.transform_lines(line_index_iterator, transform_callback) 742 | 743 | assert len(line_index_iterator) >= 1 744 | new_cursor_position_row = line_index_iterator[-1] 745 | 746 | # update text buffer 747 | buffer.document = Document( 748 | new_text, 749 | Document(new_text).translate_row_col_to_index(new_cursor_position_row, 0), 750 | ) 751 | buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True) 752 | buffer._search(search_state, include_current_position=True) 753 | 754 | # update editor state 755 | editor.last_substitute_text = replace 756 | search_state.text = search 757 | -------------------------------------------------------------------------------- /pyvim/layout.py: -------------------------------------------------------------------------------- 1 | """ 2 | The actual layout for the renderer. 3 | """ 4 | from __future__ import unicode_literals 5 | from prompt_toolkit.application.current import get_app 6 | from prompt_toolkit.filters import has_focus, is_searching, Condition, has_arg 7 | from prompt_toolkit.key_binding.vi_state import InputMode 8 | from prompt_toolkit.layout import HSplit, VSplit, FloatContainer, Float, Layout 9 | from prompt_toolkit.layout.containers import Window, ConditionalContainer, ColorColumn, WindowAlign, ScrollOffsets 10 | from prompt_toolkit.layout.controls import BufferControl 11 | from prompt_toolkit.layout.controls import FormattedTextControl 12 | from prompt_toolkit.layout.dimension import Dimension 13 | from prompt_toolkit.layout.margins import ConditionalMargin, NumberedMargin 14 | from prompt_toolkit.layout.menus import CompletionsMenu 15 | from prompt_toolkit.layout.processors import Processor, ConditionalProcessor, BeforeInput, ShowTrailingWhiteSpaceProcessor, Transformation, HighlightSelectionProcessor, HighlightSearchProcessor, HighlightIncrementalSearchProcessor, HighlightMatchingBracketProcessor, TabsProcessor, DisplayMultipleCursors 16 | from prompt_toolkit.layout.utils import explode_text_fragments 17 | from prompt_toolkit.mouse_events import MouseEventType 18 | from prompt_toolkit.selection import SelectionType 19 | from prompt_toolkit.widgets.toolbars import FormattedTextToolbar, SystemToolbar, SearchToolbar, ValidationToolbar, CompletionsToolbar 20 | 21 | from .commands.lexer import create_command_lexer 22 | from .lexer import DocumentLexer 23 | from .welcome_message import WELCOME_MESSAGE_TOKENS, WELCOME_MESSAGE_HEIGHT, WELCOME_MESSAGE_WIDTH 24 | 25 | import pyvim.window_arrangement as window_arrangement 26 | from functools import partial 27 | 28 | import re 29 | import sys 30 | 31 | __all__ = ( 32 | 'EditorLayout', 33 | 'get_terminal_title', 34 | ) 35 | 36 | def _try_char(character, backup, encoding=sys.stdout.encoding): 37 | """ 38 | Return `character` if it can be encoded using sys.stdout, else return the 39 | backup character. 40 | """ 41 | if character.encode(encoding, 'replace') == b'?': 42 | return backup 43 | else: 44 | return character 45 | 46 | 47 | TABSTOP_DOT = _try_char('\u2508', '.') 48 | 49 | 50 | class TabsControl(FormattedTextControl): 51 | """ 52 | Displays the tabs at the top of the screen, when there is more than one 53 | open tab. 54 | """ 55 | def __init__(self, editor): 56 | def location_for_tab(tab): 57 | return tab.active_window.editor_buffer.get_display_name(short=True) 58 | 59 | def create_tab_handler(index): 60 | " Return a mouse handler for this tab. Select the tab on click. " 61 | def handler(app, mouse_event): 62 | if mouse_event.event_type == MouseEventType.MOUSE_DOWN: 63 | editor.window_arrangement.active_tab_index = index 64 | editor.sync_with_prompt_toolkit() 65 | else: 66 | return NotImplemented 67 | return handler 68 | 69 | def get_tokens(): 70 | selected_tab_index = editor.window_arrangement.active_tab_index 71 | 72 | result = [] 73 | append = result.append 74 | 75 | for i, tab in enumerate(editor.window_arrangement.tab_pages): 76 | caption = location_for_tab(tab) 77 | if tab.has_unsaved_changes: 78 | caption = ' + ' + caption 79 | 80 | handler = create_tab_handler(i) 81 | 82 | if i == selected_tab_index: 83 | append(('class:tabbar.tab.active', ' %s ' % caption, handler)) 84 | else: 85 | append(('class:tabbar.tab', ' %s ' % caption, handler)) 86 | append(('class:tabbar', ' ')) 87 | 88 | return result 89 | 90 | super(TabsControl, self).__init__(get_tokens, style='class:tabbar') 91 | 92 | 93 | class TabsToolbar(ConditionalContainer): 94 | def __init__(self, editor): 95 | super(TabsToolbar, self).__init__( 96 | Window(TabsControl(editor), height=1), 97 | filter=Condition(lambda: len(editor.window_arrangement.tab_pages) > 1)) 98 | 99 | 100 | class CommandLine(ConditionalContainer): 101 | """ 102 | The editor command line. (For at the bottom of the screen.) 103 | """ 104 | def __init__(self, editor): 105 | super(CommandLine, self).__init__( 106 | Window( 107 | BufferControl( 108 | buffer=editor.command_buffer, 109 | input_processors=[BeforeInput(':')], 110 | lexer=create_command_lexer()), 111 | height=1), 112 | filter=has_focus(editor.command_buffer)) 113 | 114 | 115 | class WelcomeMessageWindow(ConditionalContainer): 116 | """ 117 | Welcome message pop-up, which is shown during start-up when no other files 118 | were opened. 119 | """ 120 | def __init__(self, editor): 121 | once_hidden = [False] # Nonlocal 122 | 123 | def condition(): 124 | # Get editor buffers 125 | buffers = editor.window_arrangement.editor_buffers 126 | 127 | # Only show when there is only one empty buffer, but once the 128 | # welcome message has been hidden, don't show it again. 129 | result = (len(buffers) == 1 and buffers[0].buffer.text == '' and 130 | buffers[0].location is None and not once_hidden[0]) 131 | if not result: 132 | once_hidden[0] = True 133 | return result 134 | 135 | super(WelcomeMessageWindow, self).__init__( 136 | Window( 137 | FormattedTextControl(lambda: WELCOME_MESSAGE_TOKENS), 138 | align=WindowAlign.CENTER, 139 | style="class:welcome"), 140 | filter=Condition(condition)) 141 | 142 | 143 | def _bufferlist_overlay_visible(editor): 144 | """ 145 | True when the buffer list overlay should be displayed. 146 | (This is when someone starts typing ':b' or ':buffer' in the command line.) 147 | """ 148 | @Condition 149 | def overlay_is_visible(): 150 | app = get_app() 151 | 152 | text = editor.command_buffer.text.lstrip() 153 | return app.layout.has_focus(editor.command_buffer) and ( 154 | any(text.startswith(p) for p in ['b ', 'b! ', 'buffer', 'buffer!'])) 155 | return overlay_is_visible 156 | 157 | 158 | class BufferListOverlay(ConditionalContainer): 159 | """ 160 | Floating window that shows the list of buffers when we are typing ':b' 161 | inside the vim command line. 162 | """ 163 | def __init__(self, editor): 164 | def highlight_location(location, search_string, default_token): 165 | """ 166 | Return a tokenlist with the `search_string` highlighted. 167 | """ 168 | result = [(default_token, c) for c in location] 169 | 170 | # Replace token of matching positions. 171 | for m in re.finditer(re.escape(search_string), location): 172 | for i in range(m.start(), m.end()): 173 | result[i] = ('class:searchmatch', result[i][1]) 174 | 175 | if location == search_string: 176 | result[0] = (result[0][0] + ' [SetCursorPosition]', result[0][1]) 177 | 178 | return result 179 | 180 | def get_tokens(): 181 | wa = editor.window_arrangement 182 | buffer_infos = wa.list_open_buffers() 183 | 184 | # Filter infos according to typed text. 185 | input_params = editor.command_buffer.text.lstrip().split(None, 1) 186 | search_string = input_params[1] if len(input_params) > 1 else '' 187 | 188 | if search_string: 189 | def matches(info): 190 | """ 191 | True when we should show this entry. 192 | """ 193 | # When the input appears in the location. 194 | if input_params[1] in (info.editor_buffer.location or ''): 195 | return True 196 | 197 | # When the input matches this buffer his index number. 198 | if input_params[1] in str(info.index): 199 | return True 200 | 201 | # When this entry is part of the current completions list. 202 | b = editor.command_buffer 203 | 204 | if b.complete_state and any(info.editor_buffer.location in c.display 205 | for c in b.complete_state.completions 206 | if info.editor_buffer.location is not None): 207 | return True 208 | 209 | return False 210 | 211 | buffer_infos = [info for info in buffer_infos if matches(info)] 212 | 213 | # Render output. 214 | if len(buffer_infos) == 0: 215 | return [('', ' No match found. ')] 216 | else: 217 | result = [] 218 | 219 | # Create title. 220 | result.append(('', ' ')) 221 | result.append(('class:title', 'Open buffers\n')) 222 | 223 | # Get length of longest location 224 | max_location_len = max(len(info.editor_buffer.get_display_name()) for info in buffer_infos) 225 | 226 | # Show info for each buffer. 227 | for info in buffer_infos: 228 | eb = info.editor_buffer 229 | char = '%' if info.is_active else ' ' 230 | char2 = 'a' if info.is_visible else ' ' 231 | char3 = ' + ' if info.editor_buffer.has_unsaved_changes else ' ' 232 | t = 'class:active' if info.is_active else '' 233 | 234 | result.extend([ 235 | ('', ' '), 236 | (t, '%3i ' % info.index), 237 | (t, '%s' % char), 238 | (t, '%s ' % char2), 239 | (t, '%s ' % char3), 240 | ]) 241 | result.extend(highlight_location(eb.get_display_name(), search_string, t)) 242 | result.extend([ 243 | (t, ' ' * (max_location_len - len(eb.get_display_name()))), 244 | (t + ' class:lineno', ' line %i' % (eb.buffer.document.cursor_position_row + 1)), 245 | (t, ' \n') 246 | ]) 247 | return result 248 | 249 | super(BufferListOverlay, self).__init__( 250 | Window(FormattedTextControl(get_tokens), 251 | style='class:bufferlist', 252 | scroll_offsets=ScrollOffsets(top=1, bottom=1)), 253 | filter=_bufferlist_overlay_visible(editor)) 254 | 255 | 256 | class MessageToolbarBar(ConditionalContainer): 257 | """ 258 | Pop-up (at the bottom) for showing error/status messages. 259 | """ 260 | def __init__(self, editor): 261 | def get_tokens(): 262 | if editor.message: 263 | return [('class:message', editor.message)] 264 | else: 265 | return [] 266 | 267 | super(MessageToolbarBar, self).__init__( 268 | FormattedTextToolbar(get_tokens), 269 | filter=Condition(lambda: editor.message is not None)) 270 | 271 | 272 | class ReportMessageToolbar(ConditionalContainer): 273 | """ 274 | Toolbar that shows the messages, given by the reporter. 275 | (It shows the error message, related to the current line.) 276 | """ 277 | def __init__(self, editor): 278 | def get_formatted_text(): 279 | eb = editor.window_arrangement.active_editor_buffer 280 | 281 | lineno = eb.buffer.document.cursor_position_row 282 | errors = eb.report_errors 283 | 284 | for e in errors: 285 | if e.lineno == lineno: 286 | return e.formatted_text 287 | 288 | return [] 289 | 290 | super(ReportMessageToolbar, self).__init__( 291 | FormattedTextToolbar(get_formatted_text), 292 | filter=~has_focus(editor.command_buffer) & ~is_searching & ~has_focus('system')) 293 | 294 | 295 | class WindowStatusBar(FormattedTextToolbar): 296 | """ 297 | The status bar, which is shown below each window in a tab page. 298 | """ 299 | def __init__(self, editor, editor_buffer): 300 | def get_text(): 301 | app = get_app() 302 | 303 | insert_mode = app.vi_state.input_mode in (InputMode.INSERT, InputMode.INSERT_MULTIPLE) 304 | replace_mode = app.vi_state.input_mode == InputMode.REPLACE 305 | sel = editor_buffer.buffer.selection_state 306 | temp_navigation = app.vi_state.temporary_navigation_mode 307 | visual_line = sel is not None and sel.type == SelectionType.LINES 308 | visual_block = sel is not None and sel.type == SelectionType.BLOCK 309 | visual_char = sel is not None and sel.type == SelectionType.CHARACTERS 310 | 311 | def mode(): 312 | if get_app().layout.has_focus(editor_buffer.buffer): 313 | if insert_mode: 314 | if temp_navigation: 315 | return ' -- (insert) --' 316 | elif editor.paste_mode: 317 | return ' -- INSERT (paste)--' 318 | else: 319 | return ' -- INSERT --' 320 | elif replace_mode: 321 | if temp_navigation: 322 | return ' -- (replace) --' 323 | else: 324 | return ' -- REPLACE --' 325 | elif visual_block: 326 | return ' -- VISUAL BLOCK --' 327 | elif visual_line: 328 | return ' -- VISUAL LINE --' 329 | elif visual_char: 330 | return ' -- VISUAL --' 331 | return ' ' 332 | 333 | def recording(): 334 | if app.vi_state.recording_register: 335 | return 'recording ' 336 | else: 337 | return '' 338 | 339 | return ''.join([ 340 | ' ', 341 | recording(), 342 | (editor_buffer.location or ''), 343 | (' [New File]' if editor_buffer.is_new else ''), 344 | ('*' if editor_buffer.has_unsaved_changes else ''), 345 | (' '), 346 | mode(), 347 | ]) 348 | super(WindowStatusBar, self).__init__( 349 | get_text, 350 | style='class:toolbar.status') 351 | 352 | 353 | class WindowStatusBarRuler(ConditionalContainer): 354 | """ 355 | The right side of the Vim toolbar, showing the location of the cursor in 356 | the file, and the vectical scroll percentage. 357 | """ 358 | def __init__(self, editor, buffer_window, buffer): 359 | def get_scroll_text(): 360 | info = buffer_window.render_info 361 | 362 | if info: 363 | if info.full_height_visible: 364 | return 'All' 365 | elif info.top_visible: 366 | return 'Top' 367 | elif info.bottom_visible: 368 | return 'Bot' 369 | else: 370 | percentage = info.vertical_scroll_percentage 371 | return '%2i%%' % percentage 372 | 373 | return '' 374 | 375 | def get_tokens(): 376 | main_document = buffer.document 377 | 378 | return [ 379 | ('class:cursorposition', '(%i,%i)' % (main_document.cursor_position_row + 1, 380 | main_document.cursor_position_col + 1)), 381 | ('', ' - '), 382 | ('class:percentage', get_scroll_text()), 383 | ('', ' '), 384 | ] 385 | 386 | super(WindowStatusBarRuler, self).__init__( 387 | Window( 388 | FormattedTextControl(get_tokens), 389 | char=' ', 390 | align=WindowAlign.RIGHT, 391 | style='class:toolbar.status', 392 | height=1, 393 | ), 394 | filter=Condition(lambda: editor.show_ruler)) 395 | 396 | 397 | class SimpleArgToolbar(ConditionalContainer): 398 | """ 399 | Simple control showing the Vi repeat arg. 400 | """ 401 | def __init__(self): 402 | def get_tokens(): 403 | arg = get_app().key_processor.arg 404 | if arg is not None: 405 | return [('class:arg', ' %s ' % arg)] 406 | else: 407 | return [] 408 | 409 | super(SimpleArgToolbar, self).__init__( 410 | Window(FormattedTextControl(get_tokens), align=WindowAlign.RIGHT), 411 | filter=has_arg), 412 | 413 | 414 | class PyvimScrollOffsets(ScrollOffsets): 415 | def __init__(self, editor): 416 | self.editor = editor 417 | self.left = 0 418 | self.right = 0 419 | 420 | @property 421 | def top(self): 422 | return self.editor.scroll_offset 423 | 424 | @property 425 | def bottom(self): 426 | return self.editor.scroll_offset 427 | 428 | 429 | class EditorLayout(object): 430 | """ 431 | The main layout class. 432 | """ 433 | def __init__(self, editor, window_arrangement): 434 | self.editor = editor # Back reference to editor. 435 | self.window_arrangement = window_arrangement 436 | 437 | # Mapping from (`window_arrangement.Window`, `EditorBuffer`) to a frame 438 | # (Layout instance). 439 | # We keep this as a cache in order to easily reuse the same frames when 440 | # the layout is updated. (We don't want to create new frames on every 441 | # update call, because that way, we would loose some state, like the 442 | # vertical scroll offset.) 443 | self._frames = {} 444 | 445 | self._fc = FloatContainer( 446 | content=VSplit([ 447 | Window(BufferControl()) # Dummy window 448 | ]), 449 | floats=[ 450 | Float(xcursor=True, ycursor=True, 451 | content=CompletionsMenu(max_height=12, 452 | scroll_offset=2, 453 | extra_filter=~has_focus(editor.command_buffer))), 454 | Float(content=BufferListOverlay(editor), bottom=1, left=0), 455 | Float(bottom=1, left=0, right=0, height=1, 456 | content=ConditionalContainer( 457 | CompletionsToolbar(), 458 | filter=has_focus(editor.command_buffer) & 459 | ~_bufferlist_overlay_visible(editor) & 460 | Condition(lambda: editor.show_wildmenu))), 461 | Float(bottom=1, left=0, right=0, height=1, 462 | content=ValidationToolbar()), 463 | Float(bottom=1, left=0, right=0, height=1, 464 | content=MessageToolbarBar(editor)), 465 | Float(content=WelcomeMessageWindow(editor), 466 | height=WELCOME_MESSAGE_HEIGHT, 467 | width=WELCOME_MESSAGE_WIDTH), 468 | ] 469 | ) 470 | 471 | search_toolbar = SearchToolbar(vi_mode=True, search_buffer=editor.search_buffer) 472 | self.search_control = search_toolbar.control 473 | 474 | self.layout = Layout(FloatContainer( 475 | content=HSplit([ 476 | TabsToolbar(editor), 477 | self._fc, 478 | CommandLine(editor), 479 | ReportMessageToolbar(editor), 480 | SystemToolbar(), 481 | search_toolbar, 482 | ]), 483 | floats=[ 484 | Float(right=0, height=1, bottom=0, width=5, 485 | content=SimpleArgToolbar()), 486 | ] 487 | )) 488 | 489 | def get_vertical_border_char(self): 490 | " Return the character to be used for the vertical border. " 491 | return _try_char('\u2502', '|', get_app().output.encoding()) 492 | 493 | def update(self): 494 | """ 495 | Update layout to match the layout as described in the 496 | WindowArrangement. 497 | """ 498 | # Start with an empty frames list everytime, to avoid memory leaks. 499 | existing_frames = self._frames 500 | self._frames = {} 501 | 502 | def create_layout_from_node(node): 503 | if isinstance(node, window_arrangement.Window): 504 | # Create frame for Window, or reuse it, if we had one already. 505 | key = (node, node.editor_buffer) 506 | frame = existing_frames.get(key) 507 | if frame is None: 508 | frame, pt_window = self._create_window_frame(node.editor_buffer) 509 | 510 | # Link layout Window to arrangement. 511 | node.pt_window = pt_window 512 | 513 | self._frames[key] = frame 514 | return frame 515 | 516 | elif isinstance(node, window_arrangement.VSplit): 517 | return VSplit( 518 | [create_layout_from_node(n) for n in node], 519 | padding=1, 520 | padding_char=self.get_vertical_border_char(), 521 | padding_style='class:frameborder') 522 | 523 | if isinstance(node, window_arrangement.HSplit): 524 | return HSplit([create_layout_from_node(n) for n in node]) 525 | 526 | layout = create_layout_from_node(self.window_arrangement.active_tab.root) 527 | self._fc.content = layout 528 | 529 | def _create_window_frame(self, editor_buffer): 530 | """ 531 | Create a Window for the buffer, with underneath a status bar. 532 | """ 533 | @Condition 534 | def wrap_lines(): 535 | return self.editor.wrap_lines 536 | 537 | window = Window( 538 | self._create_buffer_control(editor_buffer), 539 | allow_scroll_beyond_bottom=True, 540 | scroll_offsets=ScrollOffsets( 541 | left=0, right=0, 542 | top=(lambda: self.editor.scroll_offset), 543 | bottom=(lambda: self.editor.scroll_offset)), 544 | wrap_lines=wrap_lines, 545 | left_margins=[ConditionalMargin( 546 | margin=NumberedMargin( 547 | display_tildes=True, 548 | relative=Condition(lambda: self.editor.relative_number)), 549 | filter=Condition(lambda: self.editor.show_line_numbers))], 550 | cursorline=Condition(lambda: self.editor.cursorline), 551 | cursorcolumn=Condition(lambda: self.editor.cursorcolumn), 552 | colorcolumns=( 553 | lambda: [ColorColumn(pos) for pos in self.editor.colorcolumn]), 554 | ignore_content_width=True, 555 | ignore_content_height=True, 556 | get_line_prefix=partial(self._get_line_prefix, editor_buffer.buffer)) 557 | 558 | return HSplit([ 559 | window, 560 | VSplit([ 561 | WindowStatusBar(self.editor, editor_buffer), 562 | WindowStatusBarRuler(self.editor, window, editor_buffer.buffer), 563 | ], width=Dimension()), # Ignore actual status bar width. 564 | ]), window 565 | 566 | def _create_buffer_control(self, editor_buffer): 567 | """ 568 | Create a new BufferControl for a given location. 569 | """ 570 | @Condition 571 | def preview_search(): 572 | return self.editor.incsearch 573 | 574 | input_processors = [ 575 | # Processor for visualising spaces. (should come before the 576 | # selection processor, otherwise, we won't see these spaces 577 | # selected.) 578 | ConditionalProcessor( 579 | ShowTrailingWhiteSpaceProcessor(), 580 | Condition(lambda: self.editor.display_unprintable_characters)), 581 | 582 | # Replace tabs by spaces. 583 | TabsProcessor( 584 | tabstop=(lambda: self.editor.tabstop), 585 | char1=(lambda: '|' if self.editor.display_unprintable_characters else ' '), 586 | char2=(lambda: _try_char('\u2508', '.', get_app().output.encoding()) 587 | if self.editor.display_unprintable_characters else ' '), 588 | ), 589 | 590 | # Reporting of errors, for Pyflakes. 591 | ReportingProcessor(editor_buffer), 592 | HighlightSelectionProcessor(), 593 | ConditionalProcessor( 594 | HighlightSearchProcessor(), 595 | Condition(lambda: self.editor.highlight_search)), 596 | ConditionalProcessor( 597 | HighlightIncrementalSearchProcessor(), 598 | Condition(lambda: self.editor.highlight_search) & preview_search), 599 | HighlightMatchingBracketProcessor(), 600 | DisplayMultipleCursors(), 601 | ] 602 | 603 | return BufferControl( 604 | lexer=DocumentLexer(editor_buffer), 605 | include_default_input_processors=False, 606 | input_processors=input_processors, 607 | buffer=editor_buffer.buffer, 608 | preview_search=preview_search, 609 | search_buffer_control=self.search_control, 610 | focus_on_click=True) 611 | 612 | def _get_line_prefix(self, buffer, line_number, wrap_count): 613 | if wrap_count > 0: 614 | result = [] 615 | 616 | # Add 'breakindent' prefix. 617 | if self.editor.break_indent: 618 | line = buffer.document.lines[line_number] 619 | prefix = line[:len(line) - len(line.lstrip())] 620 | result.append(('', prefix)) 621 | 622 | # Add softwrap mark. 623 | result.append(('class:soft-wrap', '...')) 624 | return result 625 | return '' 626 | 627 | class ReportingProcessor(Processor): 628 | """ 629 | Highlight all pyflakes errors on the input. 630 | """ 631 | def __init__(self, editor_buffer): 632 | self.editor_buffer = editor_buffer 633 | 634 | def apply_transformation(self, transformation_input): 635 | fragments = transformation_input.fragments 636 | 637 | if self.editor_buffer.report_errors: 638 | for error in self.editor_buffer.report_errors: 639 | if error.lineno == transformation_input.lineno: 640 | fragments = explode_text_fragments(fragments) 641 | for i in range(error.start_column, error.end_column): 642 | if i < len(fragments): 643 | fragments[i] = ('class:flakeserror', fragments[i][1]) 644 | 645 | return Transformation(fragments) 646 | 647 | 648 | 649 | def get_terminal_title(editor): 650 | """ 651 | Return the terminal title, 652 | e.g.: "filename.py (/directory) - Pyvim" 653 | """ 654 | eb = editor.current_editor_buffer 655 | if eb is not None: 656 | return '%s - Pyvim' % (eb.location or '[New file]', ) 657 | else: 658 | return 'Pyvim' 659 | --------------------------------------------------------------------------------