Source code for prompt.prompt

"""Prompt module."""
import re
import copy
from datetime import timedelta

ACTION_KEYSTROKE_PATTERN = re.compile(r'<(\w+:\w+)>')

ESCAPE_ECHO = str.maketrans({
    '"': '\\"',
    '\\': '\\\\',
})

IMPRINTABLE_REPRESENTS = {
    '\a': '^G',
    '\b': '^H',             # NOTE: Neovim: <BS>, Vim: ^H. Follow Vim.
    '\t': '^I',
    '\n': '^J',
    '\v': '^K',
    '\f': '^L',
    '\r': '^M',
    '\udc80\udcffX': '^@',  # NOTE: ^0 representation in Vim.
}

IMPRINTABLE_PATTERN = re.compile(r'(%s)' % '|'.join(
    IMPRINTABLE_REPRESENTS.keys()
))


STATUS_PROGRESS = 0
STATUS_ACCEPT = 1
STATUS_CANCEL = 2
STATUS_ERROR = 3

INSERT_MODE_INSERT = 1
INSERT_MODE_REPLACE = 2


[docs]class Prompt: """Prompt class.""" prefix = '' def __init__(self, nvim, context): """Constructor. Args: nvim (neovim.Nvim): A ``neovim.Nvim`` instance. context (Context): A ``prompt.context.Context`` instance. """ from .caret import Caret from .history import History from .keymap import DEFAULT_KEYMAP_RULES, Keymap from .action import DEFAULT_ACTION self.nvim = nvim self.insert_mode = INSERT_MODE_INSERT self.context = context self.caret = Caret(context) self.history = History(self) self.action = copy.copy(DEFAULT_ACTION) self.keymap = Keymap.from_rules(nvim, DEFAULT_KEYMAP_RULES) @property def text(self): """str: A current context text. It automatically adjust the current caret locus to the tail of the text if any text is assigned. It calls the following overridable methods in order of the appearance. - on_init - Only once - on_update - on_redraw - on_keypress - on_term - Only once Example: >>> from .context import Context >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> context = Context() >>> context.text = "Hello" >>> context.caret_locus = 3 >>> prompt = Prompt(nvim, context) >>> prompt.text 'Hello' >>> prompt.caret.locus 3 >>> prompt.text = "FooFooFoo" >>> prompt.text 'FooFooFoo' >>> prompt.caret.locus 9 """ return self.context.text @text.setter def text(self, value): self.context.text = value self.caret.locus = len(value)
[docs] def apply_custom_mappings_from_vim_variable(self, varname): """Apply custom key mappings from Vim variable. Args: varname (str): A global Vim's variable name """ if varname in self.nvim.vars: custom_mappings = self.nvim.vars[varname] for rule in custom_mappings: self.keymap.register_from_rule(self.nvim, rule)
[docs] def insert_text(self, text): """Insert text after the caret. Args: text (str): A text which will be inserted after the caret. Example: >>> from .context import Context >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> context = Context() >>> context.text = "Hello Goodbye" >>> context.caret_locus = 3 >>> prompt = Prompt(nvim, context) >>> prompt.insert_text('AA') >>> prompt.text 'HelAAlo Goodbye' """ locus = self.caret.locus self.text = ''.join([ self.caret.get_backward_text(), text, self.caret.get_selected_text(), self.caret.get_forward_text(), ]) self.caret.locus = locus + len(text)
[docs] def replace_text(self, text): """Replace text after the caret. Args: text (str): A text which will be replaced after the caret. Example: >>> from .context import Context >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> context = Context() >>> context.text = "Hello Goodbye" >>> context.caret_locus = 3 >>> prompt = Prompt(nvim, context) >>> prompt.replace_text('AA') >>> prompt.text 'HelAA Goodbye' """ locus = self.caret.locus self.text = ''.join([ self.caret.get_backward_text(), text, self.caret.get_forward_text()[len(text) - 1:], ]) self.caret.locus = locus + len(text)
[docs] def update_text(self, text): """Insert or replace text after the caret. Args: text (str): A text which will be replaced after the caret. Example: >>> from .context import Context >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> context = Context() >>> context.text = "Hello Goodbye" >>> context.caret_locus = 3 >>> prompt = Prompt(nvim, context) >>> prompt.insert_mode = INSERT_MODE_INSERT >>> prompt.update_text('AA') >>> prompt.text 'HelAAlo Goodbye' >>> prompt.insert_mode = INSERT_MODE_REPLACE >>> prompt.update_text('BB') >>> prompt.text 'HelAABB Goodbye' """ if self.insert_mode == INSERT_MODE_INSERT: self.insert_text(text) else: self.replace_text(text)
[docs] def redraw_prompt(self): # NOTE: # There is a highlight name 'Cursor' but some sometime the visibility # is quite low (e.g. tender) so use 'IncSearch' instead while the # visibility is quite good and most recent colorscheme care about it. backward_text = self.caret.get_backward_text() selected_text = self.caret.get_selected_text() forward_text = self.caret.get_forward_text() self.nvim.command('|'.join([ 'redraw', _build_echon_expr('Question', self.prefix), _build_echon_expr('None', backward_text), _build_echon_expr('IncSearch', selected_text), _build_echon_expr('None', forward_text), ]))
[docs] def start(self, default=None): """Start prompt with ``default`` text and return value. Args: default (None or str): A default text of the prompt. If omitted, a text in the context specified in the constructor is used. Returns: int: The status of the prompt. """ status = self.on_init(default) or STATUS_PROGRESS if self.nvim.options['timeout']: timeoutlen = timedelta( milliseconds=int(self.nvim.options['timeoutlen']) ) else: timeoutlen = None try: status = self.on_update(status) or STATUS_PROGRESS while status is STATUS_PROGRESS: self.on_redraw() status = self.on_keypress( self.keymap.harvest(self.nvim, timeoutlen) ) or STATUS_PROGRESS status = self.on_update(status) or STATUS_PROGRESS except KeyboardInterrupt: status = STATUS_CANCEL except self.nvim.error as e: self.nvim.command('|'.join([ 'echoerr "%s"' % line.translate(ESCAPE_ECHO) for line in str(e).splitlines() ])) status = STATUS_ERROR self.nvim.command('redraw!') if self.text: self.nvim.call('histadd', 'input', self.text) return self.on_term(status)
[docs] def on_init(self, default): """Initialize the prompt. It calls 'inputsave' function in Vim and assign ``default`` text to the ``self.text`` to initialize the prompt text in default. Args: default (None or str): A default text of the prompt. If omitted, a text in the context specified in the constructor is used. Returns: None or int: The return value will be used as a status of the prompt mainloop, indicating that if return value is not STATUS_PROGRESS, the prompt mainloop immediately terminated. Returning None is equal to returning STATUS_PROGRESS. """ self.nvim.call('inputsave') if default: self.text = default
[docs] def on_update(self, status): """Update the prompt status and return the status. It is used to update the prompt status. In default, it does nothing and return the specified ``status`` directly. Args: status (int): A prompt status which is updated by previous on_keypress call. Returns: None or int: The return value will be used as a status of the prompt mainloop, indicating that if return value is not STATUS_PROGRESS, the prompt mainloop immediately terminated. Returning None is equal to returning STATUS_PROGRESS. """ return status
[docs] def on_redraw(self): """Redraw the prompt. It is used to redraw the prompt. In default, it echos specified prefix the caret, and input text. """ self.redraw_prompt()
[docs] def on_keypress(self, keystroke): """Handle a pressed keystroke and return the status. It is used to handle a pressed keystroke. Note that subclass should NOT override this method to perform actions. Register a new custom action instead. In default, it call action and return the result if the keystroke is <xxx:xxx>or call Vim function XXX and return the result if the keystroke is <call:XXX>. Args: keystroke (Keystroke): A pressed keystroke instance. Note that this instance is a reslved keystroke instace by keymap. Returns: None or int: The return value will be used as a status of the prompt mainloop, indicating that if return value is not STATUS_PROGRESS, the prompt mainloop immediately terminated. Returning None is equal to returning STATUS_PROGRESS. """ m = ACTION_KEYSTROKE_PATTERN.match(str(keystroke)) if m: return self.action.call(self, m.group(1)) else: self.update_text(str(keystroke))
[docs] def on_term(self, status): """Finalize the prompt. It calls 'inputrestore' function in Vim to finalize the prompt in default. The return value is used as a return value of the prompt. Args: status (int): A prompt status. Returns: int: A status which is used as a result value of the prompt. """ self.nvim.call('inputrestore') return status
def _build_echon_expr(hl, text): if not IMPRINTABLE_PATTERN.search(text): return 'echohl %s|echon "%s"' % ( hl, text.translate(ESCAPE_ECHO) ) p = 'echohl %s|echon "%%s"' % hl i = 'echohl %s|echon "%%s"' % ('SpecialKey' if hl == 'None' else hl) return '|'.join( p % term if index % 2 == 0 else i % IMPRINTABLE_REPRESENTS[term] for index, term in enumerate(IMPRINTABLE_PATTERN.split(text)) )