Ejemplo n.º 1
0
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Some error needs to be notified with a bell
        self.bell = False

        # Typing overwrite mode
        self.overwrite = False

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        # Selection history for extend/shrink selection (before_cursor, after_cursor, selection_start, extend_separators) tuple
        self.selection_history = []

        # Search string
        self.search_substr = None
        self.search_rev = False

        # List of delimiters for the "extend-selection" feature
        self.extend_separators = None

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_SEARCH_RIGHT: self.key_search_right,
            ActionCode.ACTION_SEARCH_LEFT: self.key_search_left,
            ActionCode.ACTION_SELECT_UP: self.key_extend_selection,
            ActionCode.ACTION_SELECT_DOWN: self.key_shrink_selection,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs,
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_TOGGLE_OVERWRITE: self.key_toggle_overwrite, }

        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE,
                               ActionCode.ACTION_DELETE_WORD,
                               ActionCode.ACTION_BACKSPACE,
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT,
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME,
                                 ActionCode.ACTION_END,
                                 ActionCode.ACTION_SEARCH_RIGHT,
                                 ActionCode.ACTION_SEARCH_LEFT,
                                 ActionCode.ACTION_SELECT_UP]
        self.manip_actions = [ActionCode.ACTION_CUT,
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions
Ejemplo n.º 2
0
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        self.open_app = os.path.expandvars("%PYCMD_OPEN_APP%")
        if '%' in self.open_app:
            print('%PYCMD_OPEN_APP% is not defined!')
            self.open_app = ''

        self.user32_dll = ctypes.windll.user32

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_OPEN_CLIPBOARD: self.open_clip_board,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_SWITCH_TO_GVIM: self.switch_to_gvim,
            }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions
Ejemplo n.º 3
0
class InputState:
    """
    Handles the current state of the input line:
        * user input chars
        * displaying the prompt and command line
        * handling text selection and Cut/Copy/Paste
        * the command history
        * dynamic expansion based on the input history
    """

    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Some error needs to be notified with a bell
        self.bell = False

        # Typing overwrite mode
        self.overwrite = False

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        # Selection history for extend/shrink selection (before_cursor, after_cursor, selection_start, extend_separators) tuple
        self.selection_history = []

        # Search string
        self.search_substr = None
        self.search_rev = False

        # List of delimiters for the "extend-selection" feature
        self.extend_separators = None

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_SEARCH_RIGHT: self.key_search_right,
            ActionCode.ACTION_SEARCH_LEFT: self.key_search_left,
            ActionCode.ACTION_SELECT_UP: self.key_extend_selection,
            ActionCode.ACTION_SELECT_DOWN: self.key_shrink_selection,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs,
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_TOGGLE_OVERWRITE: self.key_toggle_overwrite, }

        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE,
                               ActionCode.ACTION_DELETE_WORD,
                               ActionCode.ACTION_BACKSPACE,
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT,
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME,
                                 ActionCode.ACTION_END,
                                 ActionCode.ACTION_SEARCH_RIGHT,
                                 ActionCode.ACTION_SEARCH_LEFT,
                                 ActionCode.ACTION_SELECT_UP]
        self.manip_actions = [ActionCode.ACTION_CUT,
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions


    def step_line(self):
        """Prepare for a new key event"""
        self.prev_prompt = self.prompt
        self.prev_before_cursor = self.before_cursor
        self.prev_after_cursor = self.after_cursor

    def reset_line(self, prompt):
        """Prepare for a new input line"""
        self.prompt = prompt
        self.before_cursor = ''
        self.after_cursor = ''
        self.overwrite = False
        self.reset_prev_line()

    def reset_prev_line(self):
        """Reset previous line (current line will repaint as new)"""
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

    def changed(self):
        """Check whether a change has occurred in the input state (e.g. for repaint)"""
        return self.prompt != self.prev_prompt \
               or self.before_cursor != self.prev_before_cursor \
               or self.after_cursor != self.prev_after_cursor

    def handle(self, action, arg = None):
        """Handle a keyboard action"""
        handler = self.handlers[action]
        if action in self.navigate_actions:
            # Navigation actions have a "select" argument
            handler(arg)
        elif action in self.insert_actions:
            # Insert actions have a "text" argument
            handler(arg)
        else:
            # Other actions don't have arguments
            handler()

        # Add the previous state as an undo state if needed
        if self.changed():
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                                and action != self.last_action) \
                    or action == ActionCode.ACTION_UNDO_EMACS:
                self.undo.append((self.prev_before_cursor, self.prev_after_cursor))
                self.redo = []
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                                and action != self.last_action) \
                    or action == ActionCode.ACTION_UNDO:
                self.undo_emacs.append((self.prev_before_cursor, self.prev_after_cursor))
                self.undo_emacs_index = -1

        # print "\n", self.undo, "    ", self.redo, "\n"

        self.last_action = action


    def key_left(self, select=False):
        """
        Move cursor one position to the left
        Also handle text selection according to flag
        """
        if self.before_cursor != '':
            self.after_cursor = self.before_cursor[-1] + self.after_cursor
            self.before_cursor = self.before_cursor[0 : -1]
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_right(self, select=False):
        """
        Move cursor one position to the right
        Also handle text selection according to flag
        """
        if self.after_cursor != '':
            self.before_cursor = self.before_cursor + self.after_cursor[0]
            self.after_cursor = self.after_cursor[1 : ]
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_home(self, select=False):
        """
        Home key
        Also handle text selection according to flag
        """
        self.after_cursor = self.before_cursor + self.after_cursor
        self.before_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_end(self, select=False):
        """
        End key
        Also handle text selection according to flag
        """
        self.before_cursor = self.before_cursor + self.after_cursor
        self.after_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_search_right(self, _):
        """
        Search for text to the right of the cursor
        """
        if (self.before_cursor + self.after_cursor).strip() == '':
            self.bell = True
            return
        self.search_rev = False
        if self.search_substr is None:
            self.search_substr = ''
        elif self.search_substr:
            self.search_right_next()

    def key_search_left(self, _):
        """
        Search for text to the left of the cursor
        """
        if (self.before_cursor + self.after_cursor).strip() == '':
            self.bell = True
            return
        self.search_rev = True
        if self.search_substr is None:
            self.search_substr = ''
        elif self.search_substr:
            self.search_left_prev()

    def key_extend_selection(self, _):
        """
        Extend the selection "lexically, i.e. select an increasingly larger chunk going
        from: word -> filename/extension -> full filename + extension -> full file path ->
        complete command -> entire line
        """
        if self.extend_separators is None:
            self.reset_selection()
            self.history.reset()

            # stick to the closest word to the left or right
            whitespace_left = len(self.before_cursor) - len(self.before_cursor.rstrip(' '))
            whitespace_right = len(self.after_cursor) - len(self.after_cursor.lstrip(' '))
            if whitespace_left == len(self.before_cursor) or whitespace_left >= whitespace_right > 0:
                for _ in range(whitespace_right):
                    self.key_right(False)
            elif whitespace_right == len(self.after_cursor) or whitespace_right >= whitespace_left > 0:
                for _ in range(whitespace_left):
                    self.key_left(False)

            # skip over trailing backslashes
            while (self.before_cursor.endswith('\\') and
                (self.after_cursor == '' or self.after_cursor.startswith(' '))):
                self.key_left(False)

            if self.before_cursor.count('"') % 2 == 0:
                if self.before_cursor.endswith('"'):
                    self.key_left(False)
                elif self.after_cursor.startswith('"'):
                    self.key_right(False)

            if self.before_cursor.count('"') % 2 == 0:
                self.extend_separators = list(EXTEND_SEPARATORS_OUTSIDE_QUOTES)
            else:
                self.extend_separators = list(EXTEND_SEPARATORS_INSIDE_QUOTES)

        self.extend_selection()

    def key_shrink_selection(self):
        if self.selection_history:
            self.before_cursor, self.after_cursor, self.selection_start, self.extend_separators = self.selection_history.pop()
            if not self.selection_history:
                self.reset_selection()
        else:
            self.bell = True


    def key_left_word(self, select=False):
        """Move backward one word (Ctrl-Left)"""
        # Skip spaces
        while self.before_cursor != '' and self.before_cursor[-1] in word_sep:
            self.key_left(select)

        # Jump over word
        while self.before_cursor != '' and not self.before_cursor[-1] in word_sep:
            self.key_left(select)

    def key_right_word(self, select=False):
        """Move forward one word (Ctrl-Right)"""
        # Skip spaces
        while self.after_cursor != '' and self.after_cursor[0] in word_sep:
            self.key_right(select)

        # Jump over word
        while self.after_cursor != '' and not self.after_cursor[0] in word_sep:
            self.key_right(select)

    def key_backspace_word(self):
        """Delte backwards one word (Ctrl-Left), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.before_cursor != '' and self.before_cursor[-1] in word_sep:
                self.key_backspace()

            # Jump over word
            while self.before_cursor != '' and not self.before_cursor[-1] in word_sep:
                self.key_backspace()

    def key_del_word(self):
        """Delete forwards one word (Ctrl-Right), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.after_cursor != '' and self.after_cursor[0] in word_sep:
                self.key_del()

            # Jump over word
            while self.after_cursor != '' and not self.after_cursor[0] in word_sep:
                self.key_del()

    def key_del(self):
        """Delete character at cursor"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = self.after_cursor[1 : ]
            self.history.reset()
            self.reset_selection()

    def key_kill_line(self):
        """Kill the rest of the current line"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = ''
        self.history.reset()

    def key_up(self):
        """Arrow up (history previous)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        # print '\n\n', history, history_index, '\n\n'
        if not self.history.trail:
            # Start search
            self.history.start(self.before_cursor + self.after_cursor)
        if not self.history.up():
            self.bell = True
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        #print '\n\nHistory:', self.history
        #print 'Trail:', self.history_trail, '\n\n'

        self.reset_selection()

    def key_down(self):
        """Arrow down (history next)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        if self.history.down():
            self.before_cursor = self.history.current()[0]
            self.after_cursor = ''
        else:
            self.bell = True
        self.reset_selection()

    def key_esc(self):
        """Esc key"""
        if self.get_selection() != '' or self.search_substr is not None:
            self.reset_selection()
        else:
            if self.history.filter != '':
                # Reset search filter, if any
                self.history.reset()
            else:
                # Clear current line (we keep it in the history though)
                self.history.add(self.before_cursor + self.after_cursor)
                self.before_cursor = ''
                self.after_cursor = ''

    def key_backspace(self):
        """Backspace key"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.before_cursor = self.before_cursor[0 : -1]
            self.history.reset()
            self.reset_selection()

    def key_copy(self):
        """Copy selection to clipboard"""
        wclip.OpenClipboard()
        wclip.EmptyClipboard()
        wclip.SetClipboardText(self.get_selection())
        wclip.CloseClipboard()
        self.history.reset()

    def key_cut(self):
        """Cut selection to clipboard"""
        self.key_copy()
        self.delete_selection()
        self.history.reset()

    def key_paste(self):
        """Paste from clipboard"""
        wclip.OpenClipboard()
        if wclip.IsClipboardFormatAvailable(wclip.CF_TEXT):
            text = wclip.GetClipboardData()

            # Purge garbage chars that some apps put in the clipboard
            if text.find('\0') >= 0:
                text = text[:text.find('\0')]

            # Convert newlines to blanks
            text = text.replace('\r', '').replace('\n', ' ')

            # Insert into command line
            if self.get_selection() != '':
                self.delete_selection()
            self.before_cursor = self.before_cursor + text
            if self.overwrite:
                self.after_cursor = self.after_cursor[len(text):]
            self.reset_selection()
        wclip.CloseClipboard()
        self.history.reset()

    def key_insert(self, text):
        """Insert text at the current cursor position"""
        self.history.reset()

        if self.search_substr is not None:
            # Search mode
            self.search_substr += text
            if self.after_cursor.lower().startswith(text.lower()):
                self.before_cursor += self.after_cursor[:len(text)]
                self.after_cursor = self.after_cursor[len(text):]
            else:
                self.advance_search()
        else:
            # Typing mode
            if self.get_selection() != '':
                self.delete_selection()
            self.before_cursor += text
            if self.overwrite:
                self.after_cursor = self.after_cursor[len(text):]
            self.reset_selection()

    def key_complete(self, completed):
        """Update the text before cursor to match some completion"""
        if (completed.endswith(' ') and self.after_cursor.startswith(' ')) \
                or (completed.endswith('\\') and self.after_cursor.startswith('\\')):
            self.after_cursor = self.after_cursor[1:]
        if (completed.endswith('"\\') and self.after_cursor.startswith('"\\')
            or completed.endswith('" ') and self.after_cursor.startswith('" ')) :
            self.after_cursor = self.after_cursor[2:]
        chars_added = len(completed) - len(self.before_cursor)
        self.before_cursor = completed
        if self.overwrite:
            self.after_cursor = self.after_cursor[chars_added:]
        self.reset_selection()
        self.history.reset()

    def key_undo(self):
        """Undo the last action or group of actions"""
        if self.undo != []:
            self.redo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.undo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_undo_emacs(self):
        """Emacs-style undo"""
        if self.undo_emacs != []:
            if self.last_action != ActionCode.ACTION_UNDO_EMACS:
                self.undo_emacs.append((self.before_cursor, self.after_cursor))
                self.undo_emacs_index -= 1

            if len(self.undo_emacs) + self.undo_emacs_index >= 0:
                (before, after) = self.undo_emacs[self.undo_emacs_index]
                self.before_cursor = before
                self.after_cursor = after
                self.undo_emacs_index -= 1
                self.selection_start = len(before)

    def key_redo(self):
        """Redo the last action or group of actions"""
        if self.redo != []:
            self.undo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.redo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_expand(self, text):
        """
        Dynamically expand the word at the cursor.

        This expands the current token based by looking at the input
        history, similar to Emacs' Alt-/
        """
        if self.expand_matches == [] or self.last_action != ActionCode.ACTION_EXPAND:
            # Re-initialize the list of matches
            self.expand_line = self.before_cursor
            line_words = [''] + self.expand_line.split(' ')
            expand_stub = line_words[-1]
            expand_context = line_words[-2]

            context_matches = []
            no_context_matches = []
            for line in reversed(self.history.list):
                line_words = [''] + line.split(' ')
                for i in range(len(line_words) - 1, 0, -1):
                    word = line_words[i]
                    context = line_words[i - 1]
                    if (word.lower().startswith(expand_stub.lower())
                        and word.lower() != expand_stub.lower()):
                        if context.lower() == expand_context.lower():
                            context_matches.append(word)
                        else:
                            no_context_matches.append(word)

            # print '\n\n', no_context_matches, context_matches, '\n\n'

            self.expand_stub = expand_stub
            matches_set = {}
            self.expand_matches = [matches_set.setdefault(e, e)
                                   for e in context_matches + no_context_matches
                                   if e not in matches_set] + [self.expand_stub]
            self.expand_matches.reverse()
            # print '\n\n', self.expand_matches, '\n\n'

        match = self.expand_matches[-1]
        old_len_before_cursor = len(self.before_cursor)
        self.before_cursor = self.expand_line[:len(self.expand_line)
                                               - len(self.expand_stub)] + match
        if self.overwrite:
            self.after_cursor = self.after_cursor[len(self.before_cursor) - old_len_before_cursor:]
        self.reset_selection()
        self.history.reset()
        del self.expand_matches[-1]

    def key_toggle_overwrite(self):
        """Toggle typing overwrite mode"""
        self.overwrite = not self.overwrite

    def reset_selection(self):
        """Reset text selection"""
        self.selection_start = len(self.before_cursor)
        self.search_substr = None
        self.extend_separators = None
        self.selection_history = []

    def delete_selection(self):
        """Remove currently selected text"""
        len_before = len(self.before_cursor)
        if self.selection_start < len_before:
            self.before_cursor = self.before_cursor[: self.selection_start]
        else:
            self.after_cursor = self.after_cursor[self.selection_start - len_before: ]
        self.reset_selection()

    def get_selection_range(self):
        """Return the start and end indexes of the selection"""
        return (min(len(self.before_cursor), self.selection_start),
                max(len(self.before_cursor), self.selection_start))

    def get_selection(self):
        """Return the current selected text"""
        start, end = self.get_selection_range()
        return (self.before_cursor + self.after_cursor)[start: end]

    def advance_search(self):
        if not self.search_rev:
            self.search_right_next()
        else:
            self.search_left_prev()

    def search_right_next(self):
        pos = self.after_cursor.lower().find(self.search_substr.lower())
        if pos == -1:
            self.bell = True
            return
        self.selection_start = len(self.before_cursor) + pos
        pos += len(self.search_substr)
        self.before_cursor += self.after_cursor[:pos]
        self.after_cursor = self.after_cursor[pos:]

    def search_left_prev(self):
        pos = self.before_cursor.lower().rfind(self.search_substr.lower(), 0, -1)
        if pos == -1:
            self.bell = True
            return
        self.selection_start = pos
        pos += len(self.search_substr)
        self.before_cursor, self.after_cursor = \
            self.before_cursor[:pos], self.before_cursor[pos:] + self.after_cursor

    def extend_selection(self):
        line = self.before_cursor + self.after_cursor
        extend_begin = len(self.before_cursor)
        extend_end = max(self.selection_start, extend_begin)
        separators = list(self.extend_separators)
        expanded = False

        while not expanded and separators != []:
            while extend_begin >= 1 and not line[extend_begin - 1] in separators:
                extend_begin -= 1
                expanded = True
            while extend_end < len(line) and not line[extend_end] in separators:
                extend_end += 1
                expanded = True
            separators.pop(0)

            if separators == [] and self.before_cursor.count('"') % 2 == 1:
                separators = list(EXTEND_SEPARATORS_OUTSIDE_QUOTES)

        if expanded:
            self.selection_history.append((self.before_cursor, self.after_cursor, self.selection_start, self.extend_separators))
            self.before_cursor = line[:extend_begin]
            self.after_cursor = line[extend_begin:]
            self.selection_start = extend_end
            self.extend_separators = separators
        else:
            self.bell = True
Ejemplo n.º 4
0
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Typing overwrite mode
        self.overwrite = False

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        # Search string
        self.search_substr = None
        self.search_rev = False

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_SEARCH_RIGHT: self.key_search_right,
            ActionCode.ACTION_SEARCH_LEFT: self.key_search_left,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_TOGGLE_OVERWRITE: self.key_toggle_overwrite, }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END,
                                 ActionCode.ACTION_SEARCH_RIGHT,
                                 ActionCode.ACTION_SEARCH_LEFT]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions
Ejemplo n.º 5
0
class InputState:
    """
    Handles the current state of the input line:
        * user input chars
        * displaying the prompt and command line
        * handling text selection and Cut/Copy/Paste
        * the command history
        * dynamic expansion based on the input history
    """
    
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        self.open_app = os.path.expandvars("%PYCMD_OPEN_APP%")
        if '%' in self.open_app:
            print('%PYCMD_OPEN_APP% is not defined!')
            self.open_app = ''

        self.user32_dll = ctypes.windll.user32

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_OPEN_CLIPBOARD: self.open_clip_board,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_SWITCH_TO_GVIM: self.switch_to_gvim,
            }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions


    def step_line(self):
        """Prepare for a new key event"""
        self.prev_prompt = self.prompt
        self.prev_before_cursor = self.before_cursor
        self.prev_after_cursor = self.after_cursor

    def reset_line(self, prompt):
        """Prepare for a new input line"""
        self.prompt = prompt
        self.before_cursor = ''
        self.after_cursor = ''
        self.reset_prev_line()

    def reset_prev_line(self):
        """Reset previous line (current line will repaint as new)"""
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

    def changed(self):
        """Check whether a change has occurred in the input state (e.g. for repaint)"""
        return self.prompt != self.prev_prompt \
               or self.before_cursor != self.prev_before_cursor \
               or self.after_cursor != self.prev_after_cursor

    def handle(self, action, arg = None, arg2 = None):
        """Handle a keyboard action"""
        handler = self.handlers[action]
        if arg2 != None \
                and (action == ActionCode.ACTION_LEFT_WORD \
                or action == ActionCode.ACTION_RIGHT_WORD):
            handler(arg, arg2)
        elif arg != None \
                and (action in self.navigate_actions \
                    or action == ActionCode.ACTION_BACKSPACE_WORD \
                    or action == ActionCode.ACTION_DELETE_WORD) :
            # Navigation actions have a "select" argument
            handler(arg)
        elif action in self.insert_actions:
            # Insert actions have a "text" argument
            handler(arg)
        else:
            # Other actions don't have arguments
            handler()

        # Add the previous state as an undo state if needed
        if self.changed():
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO_EMACS:
                self.undo.append((self.prev_before_cursor, self.prev_after_cursor))
                self.redo = []
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO:
                self.undo_emacs.append((self.prev_before_cursor, self.prev_after_cursor))
                self.undo_emacs_index = -1

        # print "\n", self.undo, "    ", self.redo, "\n"

        self.last_action = action


    def key_left(self, select=False):
        """
        Move cursor one position to the left
        Also handle text selection according to flag
        """
        if self.before_cursor != '':
            self.after_cursor = self.before_cursor[-1] + self.after_cursor
            self.before_cursor = self.before_cursor[0 : -1]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_right(self, select=False):
        """
        Move cursor one position to the right
        Also handle text selection according to flag
        """
        if self.after_cursor != '':
            self.before_cursor = self.before_cursor + self.after_cursor[0]
            self.after_cursor = self.after_cursor[1 : ]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_home(self, select=False):
        """
        Home key
        Also handle text selection according to flag
        """
        self.after_cursor = self.before_cursor + self.after_cursor
        self.before_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_end(self, select=False):
        """
        End key
        Also handle text selection according to flag
        """
        self.before_cursor = self.before_cursor + self.after_cursor
        self.after_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()


    def key_left_word(self, select=False, sep=word_sep):
        """Move backward one word (Ctrl-Left)"""
        # Skip spaces
        while self.before_cursor != '' and self.before_cursor[-1] in  sep:
            self.key_left(select)

        # Jump over word
        while self.before_cursor != '' and not self.before_cursor[-1] in sep:
            self.key_left(select)

    def key_right_word(self, select=False, sep=word_sep):
        """Move forward one word (Ctrl-Right)"""
        # Skip spaces
        while self.after_cursor != '' and self.after_cursor[0] in sep:
            self.key_right(select)

        # Jump over word
        while self.after_cursor != '' and not self.after_cursor[0] in sep:
            self.key_right(select)

    def key_backspace_word(self, sep=word_sep):
        """Delte backwards one word (Ctrl-Left), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.before_cursor != '' and self.before_cursor[-1] in sep:
                self.key_backspace()

            # Jump over word
            while self.before_cursor != '' and not self.before_cursor[-1] in sep:
                self.key_backspace()

    def key_del_word(self, sep=word_sep):
        """Delete forwards one word (Ctrl-Right), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.after_cursor != '' and self.after_cursor[0] in sep:
                self.key_del()

            # Jump over word
            while self.after_cursor != '' and not self.after_cursor[0] in sep:
                self.key_del()
            
    def key_del(self):
        """Delete character at cursor"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = self.after_cursor[1 : ]
            self.history.reset()
            self.reset_selection()

    def key_kill_line(self):
        """Kill the rest of the current line"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = ''
        self.history.reset()

    def key_up(self):
        """Arrow up (history previous)"""

        # print '\n\n', history, history_index, '\n\n'
        if not self.history.trail:
            # Start search
            self.history.start(self.before_cursor + self.after_cursor)
        
        # don't update cursor and selection if there is no match in the command history
        if self.history.up() == True :
            # Clear undo/redo history
            # don't clear at undo/redo history at the start of key_up, since this up could be invalid
            self.undo = []
            self.redo = []
            
            self.before_cursor = self.history.current()[0]
            self.after_cursor = ''

        #print '\n\nHistory:', self.history
        #print 'Trail:', self.history_trail, '\n\n'

            self.reset_selection()

    def key_down(self):
        """Arrow down (history next)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        self.history.down()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        self.reset_selection()

    def key_esc(self):
        """Esc key"""
        if self.get_selection() != '':
            # Reset selection, if any
            self.reset_selection()
        else:
            if self.history.filter != '':
                # Reset search filter, if any
                self.history.reset()
            # else:
            # clear the current line for ESC and reset history at the same time
            # Not need for consecutive 2 ESC to clear the console input
            # Clear current line (we keep it in the history though)
            # Don't add the current line to history if canceled (not run)
            #self.history.add(self.before_cursor + self.after_cursor)
            self.before_cursor = ''
            self.after_cursor = ''

    def key_backspace(self):
        """Backspace key"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.before_cursor = self.before_cursor[0 : -1]
            self.history.reset()
            self.reset_selection()

    def key_copy(self):
        """Copy selection to clipboard"""
        """Seems this copy/paste function is no longer needed"""
        """As copy/paste is supported natively by cmd.exe"""
        hwnd = ctypes.wintypes.HWND(0)
        self.user32_dll.OpenClipboard(hwnd);
        self.user32_dll.EmptyClipboard();
        self.user32_dll.SetClipboardData(1, self.get_selection()) # 1 is CF_TEXT
        self.user32_dll.CloseClipboard();
        self.history.reset()

    def key_cut(self):
        """Cut selection to clipboard"""
        self.key_copy()
        self.delete_selection()
        self.history.reset()

    def key_paste(self):
        """Paste from clipboard"""

        text = PyCmdUtils.GetClipboardText()
        if len(text) == 0:
            return
            
        # Purge garbage chars that some apps put in the clipboard
        if text.find(b'\0') >= 0:
            text = text[:text.find(b'\0')]

        # Convert newlines to blanks
        text = text.replace(b'\r', b'').replace(b'\n', b' ')

        # Insert into command line
        if self.get_selection() != '':
            self.delete_selection()
        self.before_cursor = self.before_cursor + text.decode()
        self.reset_selection()
        self.history.reset()

    def open_clip_board(self):
        """Pass clipboard content to %PYCMD_OPEN_APP%"""
        if len(self.open_app) == 0:
            return

        hwnd = ctypes.wintypes.HWND(0)
        self.user32_dll.OpenClipboard(hwnd);
        if self.user32_dll.IsClipboardFormatAvailable(1): #1 is CF_TEXT
            text = ''
            GetClipboardData = self.user32_dll.GetClipboardData
            GetClipboardData.argtypes = [ctypes.wintypes.UINT]
            GetClipboardData.restype = ctypes.wintypes.HANDLE
            pcontents = GetClipboardData(1)
            if pcontents:
                text = ctypes.c_char_p(pcontents).value.decode('utf-8')

            #Purge garbage chars that some apps put in the clipboard
            if text.find('\0') >= 0:
                text = text[:text.find('\0')]

            if len(text) > 0:
                os.system("cmd.exe /c" + self.open_app + " " + text)
        self.user32_dll.CloseClipboard();

    def key_insert(self, text):
        """Insert text at the current cursor position"""
        self.history.reset()
        self.delete_selection()
        self.before_cursor += text
        self.reset_selection()

    def key_complete(self, completed):
        """Update the text before cursor to match some completion"""
        if (completed.endswith(' ') and self.after_cursor.startswith(' ')) \
                or (completed.endswith('\\') and self.after_cursor.startswith('\\')):
            # Avoid multiple blanks or backslashes after completing
            self.after_cursor = self.after_cursor[1:]
        self.before_cursor = completed
        self.reset_selection()
        self.history.reset()

    def key_undo(self):
        """Undo the last action or group of actions"""
        if self.undo != []:
            self.redo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.undo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_undo_emacs(self):
        """Emacs-style undo"""
        if self.undo_emacs != []:
            if self.last_action != ActionCode.ACTION_UNDO_EMACS:
                self.undo_emacs.append((self.before_cursor, self.after_cursor))
                self.undo_emacs_index -= 1

            if len(self.undo_emacs) + self.undo_emacs_index >= 0:
                (before, after) = self.undo_emacs[self.undo_emacs_index]
                self.before_cursor = before
                self.after_cursor = after
                self.undo_emacs_index -= 1
                self.selection_start = len(before)

    def key_redo(self):
        """Redo the last action or group of actions"""
        if self.redo != []:
            self.undo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.redo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_expand(self, text):
        """
        Dynamically expand the word at the cursor.

        This expands the current token based by looking at the input
        history, similar to Emacs' Alt-/
        """
        if self.expand_matches == [] or self.last_action != ActionCode.ACTION_EXPAND:
            # Re-initialize the list of matches
            self.expand_line = self.before_cursor
            line_words = [''] + self.expand_line.split(' ')
            expand_stub = line_words[-1]
            expand_context = line_words[-2]

            context_matches = []
            no_context_matches = []
            for line in reversed(self.history.list):
                line_words = [''] + line.split(' ')  #TODO: handle "
                for i in range(len(line_words) - 1, 0, -1):
                    word = line_words[i]
                    context = line_words[i - 1]
                    #if (word.lower().startswith(expand_stub.lower())
                    if ((word.lower().find(expand_stub.lower()) != -1)
                        and word.lower() != expand_stub.lower()): 
                        if context.lower() == expand_context.lower():
                            context_matches.append(word)
                        else:
                            no_context_matches.append(word)

            # print '\n\n', no_context_matches, context_matches, '\n\n'

            self.expand_stub = expand_stub
            matches_set = {}
            self.expand_matches = [matches_set.setdefault(e, e) 
                                   for e in context_matches + no_context_matches
                                   if e not in matches_set] + [self.expand_stub]
            # print '\n\n', self.expand_matches, '\n\n'

        match = self.expand_matches[0]
        self.before_cursor = self.expand_line[:len(self.expand_line) 
                                               - len(self.expand_stub)] + match
        self.reset_selection()
        self.history.reset()
        del self.expand_matches[0]

    def reset_selection(self):
        """Reset text selection"""
        self.selection_start = len(self.before_cursor)

    def delete_selection(self):
        """Remove currently selected text"""
        len_before = len(self.before_cursor)
        if self.selection_start < len_before:
            self.before_cursor = self.before_cursor[: self.selection_start]
        else:
            self.after_cursor = self.after_cursor[self.selection_start - len_before: ]
        self.reset_selection()

    def get_selection_range(self):
        """Return the start and end indexes of the selection"""
        return (min(len(self.before_cursor), self.selection_start),
                max(len(self.before_cursor), self.selection_start))

    def get_selection(self):
        """Return the current selected text"""
        start, end = self.get_selection_range()
        return (self.before_cursor + self.after_cursor)[start: end]

    def switch_to_gvim(self):
        PyCmdUtils.SwitchToGVim()
Ejemplo n.º 6
0
class InputState:
    """
    Handles the current state of the input line:
        * user input chars
        * displaying the prompt and command line
        * handling text selection and Cut/Copy/Paste
        * the command history
        * dynamic expansion based on the input history
    """
    
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Typing overwrite mode
        self.overwrite = False

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        # Search string
        self.search_substr = None
        self.search_rev = False

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_SEARCH_RIGHT: self.key_search_right,
            ActionCode.ACTION_SEARCH_LEFT: self.key_search_left,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_TOGGLE_OVERWRITE: self.key_toggle_overwrite, }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END,
                                 ActionCode.ACTION_SEARCH_RIGHT,
                                 ActionCode.ACTION_SEARCH_LEFT]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions


    def step_line(self):
        """Prepare for a new key event"""
        self.prev_prompt = self.prompt
        self.prev_before_cursor = self.before_cursor
        self.prev_after_cursor = self.after_cursor

    def reset_line(self, prompt):
        """Prepare for a new input line"""
        self.prompt = prompt
        self.before_cursor = ''
        self.after_cursor = ''
        self.overwrite = False
        self.reset_prev_line()

    def reset_prev_line(self):
        """Reset previous line (current line will repaint as new)"""
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

    def changed(self):
        """Check whether a change has occurred in the input state (e.g. for repaint)"""
        return self.prompt != self.prev_prompt \
               or self.before_cursor != self.prev_before_cursor \
               or self.after_cursor != self.prev_after_cursor

    def handle(self, action, arg = None):
        """Handle a keyboard action"""
        handler = self.handlers[action]
        if action in self.navigate_actions:
            # Navigation actions have a "select" argument
            handler(arg)
        elif action in self.insert_actions:
            # Insert actions have a "text" argument
            handler(arg)
        else:
            # Other actions don't have arguments
            handler()

        # Add the previous state as an undo state if needed
        if self.changed():
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO_EMACS:
                self.undo.append((self.prev_before_cursor, self.prev_after_cursor))
                self.redo = []
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO:
                self.undo_emacs.append((self.prev_before_cursor, self.prev_after_cursor))
                self.undo_emacs_index = -1

        # print "\n", self.undo, "    ", self.redo, "\n"

        self.last_action = action


    def key_left(self, select=False):
        """
        Move cursor one position to the left
        Also handle text selection according to flag
        """
        if self.before_cursor != '':
            self.after_cursor = self.before_cursor[-1] + self.after_cursor
            self.before_cursor = self.before_cursor[0 : -1]
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_right(self, select=False):
        """
        Move cursor one position to the right
        Also handle text selection according to flag
        """
        if self.after_cursor != '':
            self.before_cursor = self.before_cursor + self.after_cursor[0]
            self.after_cursor = self.after_cursor[1 : ]
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_home(self, select=False):
        """
        Home key
        Also handle text selection according to flag
        """
        self.after_cursor = self.before_cursor + self.after_cursor
        self.before_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_end(self, select=False):
        """
        End key
        Also handle text selection according to flag
        """
        self.before_cursor = self.before_cursor + self.after_cursor
        self.after_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()
        self.search_substr = None

    def key_search_right(self, _):
        """
        Search for text to the right of the cursor
        """
        self.search_rev = False
        if self.search_substr is None:
            self.search_substr = ''
        elif self.search_substr:
            self.search_right_next()

    def key_search_left(self, _):
        """
        Search for text to the left of the cursor
        """
        self.search_rev = True
        if self.search_substr is None:
            self.search_substr = ''
        elif self.search_substr:
            self.search_left_prev()

    def key_left_word(self, select=False):
        """Move backward one word (Ctrl-Left)"""
        # Skip spaces
        while self.before_cursor != '' and self.before_cursor[-1] in  word_sep:
            self.key_left(select)

        # Jump over word
        while self.before_cursor != '' and not self.before_cursor[-1] in word_sep:
            self.key_left(select)

    def key_right_word(self, select=False):
        """Move forward one word (Ctrl-Right)"""
        # Skip spaces
        while self.after_cursor != '' and self.after_cursor[0] in word_sep:
            self.key_right(select)

        # Jump over word
        while self.after_cursor != '' and not self.after_cursor[0] in word_sep:
            self.key_right(select)

    def key_backspace_word(self):
        """Delte backwards one word (Ctrl-Left), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.before_cursor != '' and self.before_cursor[-1] in word_sep:
                self.key_backspace()

            # Jump over word
            while self.before_cursor != '' and not self.before_cursor[-1] in word_sep:
                self.key_backspace()

    def key_del_word(self):
        """Delete forwards one word (Ctrl-Right), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.after_cursor != '' and self.after_cursor[0] in word_sep:
                self.key_del()

            # Jump over word
            while self.after_cursor != '' and not self.after_cursor[0] in word_sep:
                self.key_del()
            
    def key_del(self):
        """Delete character at cursor"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = self.after_cursor[1 : ]
            self.history.reset()
            self.reset_selection()

    def key_kill_line(self):
        """Kill the rest of the current line"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = ''
        self.history.reset()

    def key_up(self):
        """Arrow up (history previous)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        # print '\n\n', history, history_index, '\n\n'
        if not self.history.trail:
            # Start search
            self.history.start(self.before_cursor + self.after_cursor)
        self.history.up()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        #print '\n\nHistory:', self.history
        #print 'Trail:', self.history_trail, '\n\n'

        self.reset_selection()

    def key_down(self):
        """Arrow down (history next)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        self.history.down()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        self.reset_selection()

    def key_esc(self):
        """Esc key"""
        self.reset_selection()
        if self.search_substr is not None:
            # Reset text search
            self.search_substr = None
        else:
            if self.history.filter != '':
                # Reset search filter, if any
                self.history.reset()
            else:
                # Clear current line (we keep it in the history though)
                self.history.add(self.before_cursor + self.after_cursor)
                self.before_cursor = ''
                self.after_cursor = ''

    def key_backspace(self):
        """Backspace key"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.before_cursor = self.before_cursor[0 : -1]
            self.history.reset()
            self.reset_selection()

    def key_copy(self):
        """Copy selection to clipboard"""
        wclip.OpenClipboard()
        wclip.EmptyClipboard()
        wclip.SetClipboardText(self.get_selection())
        wclip.CloseClipboard()
        self.history.reset()

    def key_cut(self):
        """Cut selection to clipboard"""
        self.key_copy()
        self.delete_selection()
        self.history.reset()

    def key_paste(self):
        """Paste from clipboard"""
        wclip.OpenClipboard()
        if wclip.IsClipboardFormatAvailable(wclip.CF_TEXT):
            text = wclip.GetClipboardData()
            
            # Purge garbage chars that some apps put in the clipboard
            if text.find('\0') >= 0:
                text = text[:text.find('\0')]
            
            # Convert newlines to blanks
            text = text.replace('\r', '').replace('\n', ' ')

            # Insert into command line
            if self.get_selection() != '':
                self.delete_selection()
            self.before_cursor = self.before_cursor + text
            if self.overwrite:
                self.after_cursor = self.after_cursor[len(text):]
            self.reset_selection()
        wclip.CloseClipboard()
        self.history.reset()

    def key_insert(self, text):
        """Insert text at the current cursor position"""
        self.history.reset()

        if self.search_substr is not None:
            # Search mode
            self.search_substr += text
            if self.after_cursor.lower().startswith(text.lower()):
                self.before_cursor += self.after_cursor[:len(text)]
                self.after_cursor = self.after_cursor[len(text):]
            else:
                self.advance_search()
        else:
            # Typing mode
            self.before_cursor += text
            if self.overwrite:
                self.after_cursor = self.after_cursor[len(text):]
            self.reset_selection()
            self.delete_selection()

    def key_complete(self, completed):
        """Update the text before cursor to match some completion"""
        if (completed.endswith(' ') and self.after_cursor.startswith(' ')) \
                or (completed.endswith('\\') and self.after_cursor.startswith('\\')):
            # Avoid multiple blanks or backslashes after completing
            self.after_cursor = self.after_cursor[1:]
        chars_added = len(completed) - len(self.before_cursor)
        self.before_cursor = completed
        if self.overwrite:
            self.after_cursor = self.after_cursor[chars_added:]
        self.reset_selection()
        self.history.reset()

    def key_undo(self):
        """Undo the last action or group of actions"""
        if self.undo != []:
            self.redo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.undo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_undo_emacs(self):
        """Emacs-style undo"""
        if self.undo_emacs != []:
            if self.last_action != ActionCode.ACTION_UNDO_EMACS:
                self.undo_emacs.append((self.before_cursor, self.after_cursor))
                self.undo_emacs_index -= 1

            if len(self.undo_emacs) + self.undo_emacs_index >= 0:
                (before, after) = self.undo_emacs[self.undo_emacs_index]
                self.before_cursor = before
                self.after_cursor = after
                self.undo_emacs_index -= 1
                self.selection_start = len(before)

    def key_redo(self):
        """Redo the last action or group of actions"""
        if self.redo != []:
            self.undo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.redo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_expand(self, text):
        """
        Dynamically expand the word at the cursor.

        This expands the current token based by looking at the input
        history, similar to Emacs' Alt-/
        """
        if self.expand_matches == [] or self.last_action != ActionCode.ACTION_EXPAND:
            # Re-initialize the list of matches
            self.expand_line = self.before_cursor
            line_words = [''] + self.expand_line.split(' ')
            expand_stub = line_words[-1]
            expand_context = line_words[-2]

            context_matches = []
            no_context_matches = []
            for line in reversed(self.history.list):
                line_words = [''] + line.split(' ')
                for i in range(len(line_words) - 1, 0, -1):
                    word = line_words[i]
                    context = line_words[i - 1]
                    if (word.lower().startswith(expand_stub.lower())
                        and word.lower() != expand_stub.lower()): 
                        if context.lower() == expand_context.lower():
                            context_matches.append(word)
                        else:
                            no_context_matches.append(word)

            # print '\n\n', no_context_matches, context_matches, '\n\n'

            self.expand_stub = expand_stub
            matches_set = {}
            self.expand_matches = [matches_set.setdefault(e, e) 
                                   for e in context_matches + no_context_matches
                                   if e not in matches_set] + [self.expand_stub]
            self.expand_matches.reverse()
            # print '\n\n', self.expand_matches, '\n\n'

        match = self.expand_matches[-1]
        old_len_before_cursor = len(self.before_cursor)
        self.before_cursor = self.expand_line[:len(self.expand_line) 
                                               - len(self.expand_stub)] + match
        if self.overwrite:
            self.after_cursor = self.after_cursor[len(self.before_cursor) - old_len_before_cursor:]
        self.reset_selection()
        self.history.reset()
        del self.expand_matches[-1]

    def key_toggle_overwrite(self):
        """Toggle typing overwrite mode"""
        self.overwrite = not self.overwrite

    def reset_selection(self):
        """Reset text selection"""
        self.selection_start = len(self.before_cursor)

    def delete_selection(self):
        """Remove currently selected text"""
        len_before = len(self.before_cursor)
        if self.selection_start < len_before:
            self.before_cursor = self.before_cursor[: self.selection_start]
        else:
            self.after_cursor = self.after_cursor[self.selection_start - len_before: ]
        self.reset_selection()

    def get_selection_range(self):
        """Return the start and end indexes of the selection"""
        return (min(len(self.before_cursor), self.selection_start),
                max(len(self.before_cursor), self.selection_start))

    def get_selection(self):
        """Return the current selected text"""
        start, end = self.get_selection_range()
        return (self.before_cursor + self.after_cursor)[start: end]

    def advance_search(self):
        if not self.search_rev:
            self.search_right_next()
        else:
            self.search_left_prev()

    def search_right_next(self):
        pos = self.after_cursor.lower().find(self.search_substr.lower())
        if pos == -1:
            return
        self.selection_start = len(self.before_cursor) + pos
        pos += len(self.search_substr)
        self.before_cursor += self.after_cursor[:pos]
        self.after_cursor = self.after_cursor[pos:]

    def search_left_prev(self):
        pos = self.before_cursor.lower().rfind(self.search_substr.lower(), 0, -1)
        if pos == -1:
            return
        self.selection_start = pos
        pos += len(self.search_substr)
        self.before_cursor, self.after_cursor = \
            self.before_cursor[:pos], self.before_cursor[pos:] + self.after_cursor
Ejemplo n.º 7
0
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        self.open_app = os.path.expandvars("%PYCMD_OPEN_APP%")

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_OPEN_CLIPBOARD: self.open_clip_board,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand,
            }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions
Ejemplo n.º 8
0
class InputState:
    """
    Handles the current state of the input line:
        * user input chars
        * displaying the prompt and command line
        * handling text selection and Cut/Copy/Paste
        * the command history
        * dynamic expansion based on the input history
    """
    
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand, }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions


    def step_line(self):
        """Prepare for a new key event"""
        self.prev_prompt = self.prompt
        self.prev_before_cursor = self.before_cursor
        self.prev_after_cursor = self.after_cursor

    def reset_line(self, prompt):
        """Prepare for a new input line"""
        self.prompt = prompt
        self.before_cursor = ''
        self.after_cursor = ''
        self.reset_prev_line()

    def reset_prev_line(self):
        """Reset previous line (current line will repaint as new)"""
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

    def changed(self):
        """Check whether a change has occurred in the input state (e.g. for repaint)"""
        return self.prompt != self.prev_prompt \
               or self.before_cursor != self.prev_before_cursor \
               or self.after_cursor != self.prev_after_cursor

    def handle(self, action, arg = None):
        """Handle a keyboard action"""
        handler = self.handlers[action]
        if action in self.navigate_actions:
            # Navigation actions have a "select" argument
            handler(arg)
        elif action in self.insert_actions:
            # Insert actions have a "text" argument
            handler(arg)
        else:
            # Other actions don't have arguments
            handler()

        # Add the previous state as an undo state if needed
        if self.changed():
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO_EMACS:
                self.undo.append((self.prev_before_cursor, self.prev_after_cursor))
                self.redo = []
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO:
                self.undo_emacs.append((self.prev_before_cursor, self.prev_after_cursor))
                self.undo_emacs_index = -1

        # print "\n", self.undo, "    ", self.redo, "\n"

        self.last_action = action


    def key_left(self, select=False):
        """
        Move cursor one position to the left
        Also handle text selection according to flag
        """
        if self.before_cursor != '':
            self.after_cursor = self.before_cursor[-1] + self.after_cursor
            self.before_cursor = self.before_cursor[0 : -1]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_right(self, select=False):
        """
        Move cursor one position to the right
        Also handle text selection according to flag
        """
        if self.after_cursor != '':
            self.before_cursor = self.before_cursor + self.after_cursor[0]
            self.after_cursor = self.after_cursor[1 : ]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_home(self, select=False):
        """
        Home key
        Also handle text selection according to flag
        """
        self.after_cursor = self.before_cursor + self.after_cursor
        self.before_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_end(self, select=False):
        """
        End key
        Also handle text selection according to flag
        """
        self.before_cursor += self.after_cursor
        self.after_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()


    def key_left_word(self, select=False):
        """Move backward one word (Ctrl-Left)"""
        # Skip spaces
        while self.before_cursor != '' and self.before_cursor[-1] in  word_sep:
            self.key_left(select)

        # Jump over word
        while self.before_cursor != '' and not self.before_cursor[-1] in word_sep:
            self.key_left(select)

    def key_right_word(self, select=False):
        """Move forward one word (Ctrl-Right)"""
        # Skip spaces
        while self.after_cursor != '' and self.after_cursor[0] in word_sep:
            self.key_right(select)

        # Jump over word
        while self.after_cursor != '' and not self.after_cursor[0] in word_sep:
            self.key_right(select)

    def key_backspace_word(self):
        """Delte backwards one word (Ctrl-Left), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.before_cursor != '' and self.before_cursor[-1] in word_sep:
                self.key_backspace()

            # Jump over word
            while self.before_cursor != '' and not self.before_cursor[-1] in word_sep:
                self.key_backspace()

    def key_del_word(self):
        """Delete forwards one word (Ctrl-Right), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.after_cursor != '' and self.after_cursor[0] in word_sep:
                self.key_del()

            # Jump over word
            while self.after_cursor != '' and not self.after_cursor[0] in word_sep:
                self.key_del()
            
    def key_del(self):
        """Delete character at cursor"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = self.after_cursor[1 : ]
            self.history.reset()
            self.reset_selection()

    def key_kill_line(self):
        """Kill the rest of the current line"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = ''
        self.history.reset()

    def key_up(self):
        """Arrow up (history previous)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        # print '\n\n', history, history_index, '\n\n'
        if not self.history.trail:
            # Start search
            self.history.start(self.before_cursor + self.after_cursor)
        self.history.up()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        #print '\n\nHistory:', self.history
        #print 'Trail:', self.history_trail, '\n\n'

        self.reset_selection()

    def key_down(self):
        """Arrow down (history next)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        self.history.down()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        self.reset_selection()

    def key_esc(self):
        """Esc key"""
        if self.get_selection() != '':
            # Reset selection, if any
            self.reset_selection()
        else:
            if self.history.filter != '':
                # Reset search filter, if any
                self.history.reset()
            else:
                # Clear current line (we keep it in the history though)
                self.history.add(self.before_cursor + self.after_cursor)
                self.before_cursor = ''
                self.after_cursor = ''

    def key_backspace(self):
        """Backspace key"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.before_cursor = self.before_cursor[0 : -1]
            self.history.reset()
            self.reset_selection()

    def key_copy(self):
        """Copy selection to clipboard"""
        with wclip.Clipboard() as clip:
            text = self.get_selection()
            print text
            clip.text = text
        self.history.reset()

    def key_cut(self):
        """Cut selection to clipboard"""
        self.key_copy()
        self.delete_selection()
        self.history.reset()

    def key_paste(self):
        """Paste from clipboard"""
        with wclip.Clipboard() as clip:
            text = clip.text
            if not text: return
            # Purge garbage chars that some apps put in the clipboard
            if text.find('\0') >= 0:
                text = text[:text.find('\0')]
            
            # Convert newlines to blanks
            text = text.replace('\r', '').replace('\n', ' ')

            # Insert into command line
            if self.get_selection() != '':
                self.delete_selection()
            self.before_cursor = self.before_cursor + text
            self.reset_selection()
            self.history.reset()

    def key_insert(self, text):
        """Insert text at the current cursor position"""
        self.history.reset()
        self.delete_selection()
        self.before_cursor += text
        self.reset_selection()

    def key_complete(self, completed):
        """Update the text before cursor to match some completion"""
        if (completed.endswith(' ') and self.after_cursor.startswith(' ')) \
                or (completed.endswith('\\') and self.after_cursor.startswith('\\')):
            # Avoid multiple blanks or backslashes after completing
            self.after_cursor = self.after_cursor[1:]
        self.before_cursor = completed
        self.reset_selection()
        self.history.reset()

    def key_undo(self):
        """Undo the last action or group of actions"""
        if len(self.undo) > 0:
            self.redo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.undo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_undo_emacs(self):
        """Emacs-style undo"""
        if len(self.undo_emacs) > 0:
            if self.last_action != ActionCode.ACTION_UNDO_EMACS:
                self.undo_emacs.append((self.before_cursor, self.after_cursor))
                self.undo_emacs_index -= 1

            if len(self.undo_emacs) + self.undo_emacs_index >= 0:
                (before, after) = self.undo_emacs[self.undo_emacs_index]
                self.before_cursor = before
                self.after_cursor = after
                self.undo_emacs_index -= 1
                self.selection_start = len(before)

    def key_redo(self):
        """Redo the last action or group of actions"""
        if len(self.redo) > 0:
            self.undo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.redo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_expand(self, text):
        """
        Dynamically expand the word at the cursor.

        This expands the current token based by looking at the input
        history, similar to Emacs' Alt-/
        :type text: str
        """
        if self.expand_matches == [] or self.last_action != ActionCode.ACTION_EXPAND:
            # Re-initialize the list of matches
            self.expand_line = self.before_cursor
            line_words = [''] + self.expand_line.split(' ')
            expand_stub = line_words[-1]
            expand_context = line_words[-2]

            context_matches = []
            no_context_matches = []
            for line in reversed(self.history.list):
                line_words = [''] + line.split(' ')
                for i in range(len(line_words) - 1, 0, -1):
                    word = line_words[i]
                    context = line_words[i - 1]
                    if (word.lower().startswith(expand_stub.lower())
                        and word.lower() != expand_stub.lower()): 
                        if context.lower() == expand_context.lower():
                            context_matches.append(word)
                        else:
                            no_context_matches.append(word)

            # print '\n\n', no_context_matches, context_matches, '\n\n'

            self.expand_stub = expand_stub
            matches_set = {}
            self.expand_matches = [matches_set.setdefault(e, e) 
                                   for e in context_matches + no_context_matches
                                   if e not in matches_set] + [self.expand_stub]
            self.expand_matches.reverse()
            # print '\n\n', self.expand_matches, '\n\n'

        match = self.expand_matches[-1]
        self.before_cursor = self.expand_line[:len(self.expand_line) 
                                               - len(self.expand_stub)] + match
        self.reset_selection()
        self.history.reset()
        del self.expand_matches[-1]

    def reset_selection(self):
        """Reset text selection"""
        self.selection_start = len(self.before_cursor)

    def delete_selection(self):
        """Remove currently selected text"""
        len_before = len(self.before_cursor)
        if self.selection_start < len_before:
            self.before_cursor = self.before_cursor[: self.selection_start]
        else:
            self.after_cursor = self.after_cursor[self.selection_start - len_before: ]
        self.reset_selection()

    def get_selection_range(self):
        """Return the start and end indexes of the selection"""
        return (min(len(self.before_cursor), self.selection_start),
                max(len(self.before_cursor), self.selection_start))

    def get_selection(self):
        """Return the current selected text"""
        start, end = self.get_selection_range()
        return (self.before_cursor + self.after_cursor)[start: end]
Ejemplo n.º 9
0
class InputState:
    """
    Handles the current state of the input line:
        * user input chars
        * displaying the prompt and command line
        * handling text selection and Cut/Copy/Paste
        * the command history
        * dynamic expansion based on the input history
    """
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        self.open_app = os.path.expandvars("%PYCMD_OPEN_APP%")
        if '%' in self.open_app:
            print('%PYCMD_OPEN_APP% is not defined!')
            self.open_app = ''

        self.user32_dll = ctypes.windll.user32

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_OPEN_CLIPBOARD: self.open_clip_board,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs,
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_SWITCH_TO_GVIM: self.switch_to_gvim,
        }

        # Action categories
        self.insert_actions = [
            ActionCode.ACTION_INSERT, ActionCode.ACTION_COMPLETE,
            ActionCode.ACTION_EXPAND
        ]
        self.delete_actions = [
            ActionCode.ACTION_DELETE, ActionCode.ACTION_DELETE_WORD,
            ActionCode.ACTION_BACKSPACE, ActionCode.ACTION_BACKSPACE_WORD,
            ActionCode.ACTION_KILL_EOL
        ]
        self.navigate_actions = [
            ActionCode.ACTION_LEFT, ActionCode.ACTION_LEFT_WORD,
            ActionCode.ACTION_RIGHT, ActionCode.ACTION_RIGHT_WORD,
            ActionCode.ACTION_HOME, ActionCode.ACTION_END
        ]
        self.manip_actions = [
            ActionCode.ACTION_CUT, ActionCode.ACTION_COPY,
            ActionCode.ACTION_PASTE, ActionCode.ACTION_ESCAPE
        ]
        self.state_actions = [
            ActionCode.ACTION_UNDO, ActionCode.ACTION_REDO,
            ActionCode.ACTION_UNDO_EMACS
        ]
        self.batch_actions = [
            ActionCode.ACTION_DELETE_WORD, ActionCode.ACTION_BACKSPACE_WORD,
            ActionCode.ACTION_KILL_EOL
        ] + self.manip_actions

    def step_line(self):
        """Prepare for a new key event"""
        self.prev_prompt = self.prompt
        self.prev_before_cursor = self.before_cursor
        self.prev_after_cursor = self.after_cursor

    def reset_line(self, prompt):
        """Prepare for a new input line"""
        self.prompt = prompt
        self.before_cursor = ''
        self.after_cursor = ''
        self.reset_prev_line()

    def reset_prev_line(self):
        """Reset previous line (current line will repaint as new)"""
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

    def changed(self):
        """Check whether a change has occurred in the input state (e.g. for repaint)"""
        return self.prompt != self.prev_prompt \
               or self.before_cursor != self.prev_before_cursor \
               or self.after_cursor != self.prev_after_cursor

    def handle(self, action, arg=None, arg2=None):
        """Handle a keyboard action"""
        handler = self.handlers[action]
        if arg2 != None \
                and (action == ActionCode.ACTION_LEFT_WORD \
                or action == ActionCode.ACTION_RIGHT_WORD):
            handler(arg, arg2)
        elif arg != None \
                and (action in self.navigate_actions \
                    or action == ActionCode.ACTION_BACKSPACE_WORD \
                    or action == ActionCode.ACTION_DELETE_WORD) :
            # Navigation actions have a "select" argument
            handler(arg)
        elif action in self.insert_actions:
            # Insert actions have a "text" argument
            handler(arg)
        else:
            # Other actions don't have arguments
            handler()

        # Add the previous state as an undo state if needed
        if self.changed():
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO_EMACS:
                self.undo.append(
                    (self.prev_before_cursor, self.prev_after_cursor))
                self.redo = []
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO:
                self.undo_emacs.append(
                    (self.prev_before_cursor, self.prev_after_cursor))
                self.undo_emacs_index = -1

        # print "\n", self.undo, "    ", self.redo, "\n"

        self.last_action = action

    def key_left(self, select=False):
        """
        Move cursor one position to the left
        Also handle text selection according to flag
        """
        if self.before_cursor != '':
            self.after_cursor = self.before_cursor[-1] + self.after_cursor
            self.before_cursor = self.before_cursor[0:-1]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_right(self, select=False):
        """
        Move cursor one position to the right
        Also handle text selection according to flag
        """
        if self.after_cursor != '':
            self.before_cursor = self.before_cursor + self.after_cursor[0]
            self.after_cursor = self.after_cursor[1:]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_home(self, select=False):
        """
        Home key
        Also handle text selection according to flag
        """
        self.after_cursor = self.before_cursor + self.after_cursor
        self.before_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_end(self, select=False):
        """
        End key
        Also handle text selection according to flag
        """
        self.before_cursor = self.before_cursor + self.after_cursor
        self.after_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_left_word(self, select=False, sep=word_sep):
        """Move backward one word (Ctrl-Left)"""
        # Skip spaces
        while self.before_cursor != '' and self.before_cursor[-1] in sep:
            self.key_left(select)

        # Jump over word
        while self.before_cursor != '' and not self.before_cursor[-1] in sep:
            self.key_left(select)

    def key_right_word(self, select=False, sep=word_sep):
        """Move forward one word (Ctrl-Right)"""
        # Skip spaces
        while self.after_cursor != '' and self.after_cursor[0] in sep:
            self.key_right(select)

        # Jump over word
        while self.after_cursor != '' and not self.after_cursor[0] in sep:
            self.key_right(select)

    def key_backspace_word(self, sep=word_sep):
        """Delte backwards one word (Ctrl-Left), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.before_cursor != '' and self.before_cursor[-1] in sep:
                self.key_backspace()

            # Jump over word
            while self.before_cursor != '' and not self.before_cursor[
                    -1] in sep:
                self.key_backspace()

    def key_del_word(self, sep=word_sep):
        """Delete forwards one word (Ctrl-Right), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.after_cursor != '' and self.after_cursor[0] in sep:
                self.key_del()

            # Jump over word
            while self.after_cursor != '' and not self.after_cursor[0] in sep:
                self.key_del()

    def key_del(self):
        """Delete character at cursor"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = self.after_cursor[1:]
            self.history.reset()
            self.reset_selection()

    def key_kill_line(self):
        """Kill the rest of the current line"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = ''
        self.history.reset()

    def key_up(self):
        """Arrow up (history previous)"""

        # print '\n\n', history, history_index, '\n\n'
        if not self.history.trail:
            # Start search
            self.history.start(self.before_cursor + self.after_cursor)

        # don't update cursor and selection if there is no match in the command history
        if self.history.up() == True:
            # Clear undo/redo history
            # don't clear at undo/redo history at the start of key_up, since this up could be invalid
            self.undo = []
            self.redo = []

            self.before_cursor = self.history.current()[0]
            self.after_cursor = ''

            #print '\n\nHistory:', self.history
            #print 'Trail:', self.history_trail, '\n\n'

            self.reset_selection()

    def key_down(self):
        """Arrow down (history next)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        self.history.down()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        self.reset_selection()

    def key_esc(self):
        """Esc key"""
        if self.get_selection() != '':
            # Reset selection, if any
            self.reset_selection()
        else:
            if self.history.filter != '':
                # Reset search filter, if any
                self.history.reset()
            # else:
            # clear the current line for ESC and reset history at the same time
            # Not need for consecutive 2 ESC to clear the console input
            # Clear current line (we keep it in the history though)
            # Don't add the current line to history if canceled (not run)
            #self.history.add(self.before_cursor + self.after_cursor)
            self.before_cursor = ''
            self.after_cursor = ''

    def key_backspace(self):
        """Backspace key"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.before_cursor = self.before_cursor[0:-1]
            self.history.reset()
            self.reset_selection()

    def key_copy(self):
        """Copy selection to clipboard"""
        """Seems this copy/paste function is no longer needed"""
        """As copy/paste is supported natively by cmd.exe"""
        hwnd = ctypes.wintypes.HWND(0)
        self.user32_dll.OpenClipboard(hwnd)
        self.user32_dll.EmptyClipboard()
        self.user32_dll.SetClipboardData(1,
                                         self.get_selection())  # 1 is CF_TEXT
        self.user32_dll.CloseClipboard()
        self.history.reset()

    def key_cut(self):
        """Cut selection to clipboard"""
        self.key_copy()
        self.delete_selection()
        self.history.reset()

    def key_paste(self):
        """Paste from clipboard"""

        text = PyCmdUtils.GetClipboardText()
        if len(text) == 0:
            return

        # Purge garbage chars that some apps put in the clipboard
        if text.find(b'\0') >= 0:
            text = text[:text.find(b'\0')]

        # Convert newlines to blanks
        text = text.replace(b'\r', b'').replace(b'\n', b' ')

        # Insert into command line
        if self.get_selection() != '':
            self.delete_selection()
        self.before_cursor = self.before_cursor + text.decode()
        self.reset_selection()
        self.history.reset()

    def open_clip_board(self):
        """Pass clipboard content to %PYCMD_OPEN_APP%"""
        if len(self.open_app) == 0:
            return

        hwnd = ctypes.wintypes.HWND(0)
        self.user32_dll.OpenClipboard(hwnd)
        if self.user32_dll.IsClipboardFormatAvailable(1):  #1 is CF_TEXT
            text = ''
            GetClipboardData = self.user32_dll.GetClipboardData
            GetClipboardData.argtypes = [ctypes.wintypes.UINT]
            GetClipboardData.restype = ctypes.wintypes.HANDLE
            pcontents = GetClipboardData(1)
            if pcontents:
                text = ctypes.c_char_p(pcontents).value.decode('utf-8')

            #Purge garbage chars that some apps put in the clipboard
            if text.find('\0') >= 0:
                text = text[:text.find('\0')]

            if len(text) > 0:
                os.system("cmd.exe /c" + self.open_app + " " + text)
        self.user32_dll.CloseClipboard()

    def key_insert(self, text):
        """Insert text at the current cursor position"""
        self.history.reset()
        self.delete_selection()
        self.before_cursor += text
        self.reset_selection()

    def key_complete(self, completed):
        """Update the text before cursor to match some completion"""
        if (completed.endswith(' ') and self.after_cursor.startswith(' ')) \
                or (completed.endswith('\\') and self.after_cursor.startswith('\\')):
            # Avoid multiple blanks or backslashes after completing
            self.after_cursor = self.after_cursor[1:]
        self.before_cursor = completed
        self.reset_selection()
        self.history.reset()

    def key_undo(self):
        """Undo the last action or group of actions"""
        if self.undo != []:
            self.redo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.undo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_undo_emacs(self):
        """Emacs-style undo"""
        if self.undo_emacs != []:
            if self.last_action != ActionCode.ACTION_UNDO_EMACS:
                self.undo_emacs.append((self.before_cursor, self.after_cursor))
                self.undo_emacs_index -= 1

            if len(self.undo_emacs) + self.undo_emacs_index >= 0:
                (before, after) = self.undo_emacs[self.undo_emacs_index]
                self.before_cursor = before
                self.after_cursor = after
                self.undo_emacs_index -= 1
                self.selection_start = len(before)

    def key_redo(self):
        """Redo the last action or group of actions"""
        if self.redo != []:
            self.undo.append((self.before_cursor, self.after_cursor))
            (before, after) = self.redo.pop()
            self.before_cursor = before
            self.after_cursor = after
            self.selection_start = len(before)

    def key_expand(self, text):
        """
        Dynamically expand the word at the cursor.

        This expands the current token based by looking at the input
        history, similar to Emacs' Alt-/
        """
        if self.expand_matches == [] or self.last_action != ActionCode.ACTION_EXPAND:
            # Re-initialize the list of matches
            self.expand_line = self.before_cursor
            line_words = [''] + self.expand_line.split(' ')
            expand_stub = line_words[-1]
            expand_context = line_words[-2]

            context_matches = []
            no_context_matches = []
            for line in reversed(self.history.list):
                line_words = [''] + line.split(' ')  #TODO: handle "
                for i in range(len(line_words) - 1, 0, -1):
                    word = line_words[i]
                    context = line_words[i - 1]
                    #if (word.lower().startswith(expand_stub.lower())
                    if ((word.lower().find(expand_stub.lower()) != -1)
                            and word.lower() != expand_stub.lower()):
                        if context.lower() == expand_context.lower():
                            context_matches.append(word)
                        else:
                            no_context_matches.append(word)

            # print '\n\n', no_context_matches, context_matches, '\n\n'

            self.expand_stub = expand_stub
            matches_set = {}
            self.expand_matches = [
                matches_set.setdefault(e, e)
                for e in context_matches + no_context_matches
                if e not in matches_set
            ] + [self.expand_stub]
            # print '\n\n', self.expand_matches, '\n\n'

        match = self.expand_matches[0]
        self.before_cursor = self.expand_line[:len(self.expand_line) -
                                              len(self.expand_stub)] + match
        self.reset_selection()
        self.history.reset()
        del self.expand_matches[0]

    def reset_selection(self):
        """Reset text selection"""
        self.selection_start = len(self.before_cursor)

    def delete_selection(self):
        """Remove currently selected text"""
        len_before = len(self.before_cursor)
        if self.selection_start < len_before:
            self.before_cursor = self.before_cursor[:self.selection_start]
        else:
            self.after_cursor = self.after_cursor[self.selection_start -
                                                  len_before:]
        self.reset_selection()

    def get_selection_range(self):
        """Return the start and end indexes of the selection"""
        return (min(len(self.before_cursor), self.selection_start),
                max(len(self.before_cursor), self.selection_start))

    def get_selection(self):
        """Return the current selected text"""
        start, end = self.get_selection_range()
        return (self.before_cursor + self.after_cursor)[start:end]

    def switch_to_gvim(self):
        PyCmdUtils.SwitchToGVim()
Ejemplo n.º 10
0
class InputState:
    """
    Handles the current state of the input line:
        * user input chars
        * displaying the prompt and command line
        * handling text selection and Cut/Copy/Paste
        * the command history
        * dynamic expansion based on the input history
    """
    
    def __init__(self):
        # Current state of the input line
        self.prompt = ''
        self.before_cursor = ''
        self.after_cursor = ''

        # Previous state of the input line
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

        # Command history
        self.history = CommandHistory()

        # Text selection
        self.selection_start = 0

        # Previous line, stub and list of matches for the dynamic expansion
        self.expand_line = ''
        self.expand_stub = ''
        self.expand_matches = []

        # Line history for undo/redo - (before_cursor, after_cursor) pairs
        self.undo = []
        self.redo = []
        self.undo_emacs = []
        self.undo_emacs_index = -1
        self.last_action = ActionCode.ACTION_none

        self.open_app = os.path.expandvars("%PYCMD_OPEN_APP%")
        if '%' in self.open_app:
            print('%PYCMD_OPEN_APP% is not defined!')
            self.open_app = ''

        self.user32_dll = ctypes.windll.user32

        # Action handlers
        self.handlers = {
            ActionCode.ACTION_none: None,
            ActionCode.ACTION_LEFT: self.key_left,
            ActionCode.ACTION_RIGHT: self.key_right,
            ActionCode.ACTION_LEFT_WORD: self.key_left_word,
            ActionCode.ACTION_RIGHT_WORD: self.key_right_word,
            ActionCode.ACTION_HOME: self.key_home,
            ActionCode.ACTION_END: self.key_end,
            ActionCode.ACTION_COPY: self.key_copy,
            ActionCode.ACTION_CUT: self.key_cut,
            ActionCode.ACTION_PASTE: self.key_paste,
            ActionCode.ACTION_OPEN_CLIPBOARD: self.open_clip_board,
            ActionCode.ACTION_PREV: self.key_up,
            ActionCode.ACTION_NEXT: self.key_down,
            ActionCode.ACTION_INSERT: self.key_insert,
            ActionCode.ACTION_COMPLETE: self.key_complete,
            ActionCode.ACTION_DELETE: self.key_del,
            ActionCode.ACTION_DELETE_WORD: self.key_del_word,
            ActionCode.ACTION_BACKSPACE: self.key_backspace,
            ActionCode.ACTION_BACKSPACE_WORD: self.key_backspace_word,
            ActionCode.ACTION_KILL_EOL: self.key_kill_line,
            ActionCode.ACTION_ESCAPE: self.key_esc,
            ActionCode.ACTION_UNDO: self.key_undo,
            ActionCode.ACTION_REDO: self.key_redo,
            ActionCode.ACTION_UNDO_EMACS: self.key_undo_emacs, 
            ActionCode.ACTION_EXPAND: self.key_expand,
            ActionCode.ACTION_SWITCH_TO_GVIM: self.switch_to_gvim,
            }
            
        # Action categories
        self.insert_actions = [ActionCode.ACTION_INSERT,
                               ActionCode.ACTION_COMPLETE,
                               ActionCode.ACTION_EXPAND]
        self.delete_actions = [ActionCode.ACTION_DELETE, 
                               ActionCode.ACTION_DELETE_WORD, 
                               ActionCode.ACTION_BACKSPACE, 
                               ActionCode.ACTION_BACKSPACE_WORD,
                               ActionCode.ACTION_KILL_EOL]
        self.navigate_actions = [ActionCode.ACTION_LEFT,
                                 ActionCode.ACTION_LEFT_WORD,
                                 ActionCode.ACTION_RIGHT, 
                                 ActionCode.ACTION_RIGHT_WORD,
                                 ActionCode.ACTION_HOME, 
                                 ActionCode.ACTION_END]
        self.manip_actions = [ActionCode.ACTION_CUT, 
                              ActionCode.ACTION_COPY,
                              ActionCode.ACTION_PASTE,
                              ActionCode.ACTION_ESCAPE]
        self.state_actions = [ActionCode.ACTION_UNDO,
                              ActionCode.ACTION_REDO,
                              ActionCode.ACTION_UNDO_EMACS]
        self.batch_actions = [ActionCode.ACTION_DELETE_WORD,
                              ActionCode.ACTION_BACKSPACE_WORD,
                              ActionCode.ACTION_KILL_EOL] + self.manip_actions


    def step_line(self):
        """Prepare for a new key event"""
        self.prev_prompt = self.prompt
        self.prev_before_cursor = self.before_cursor
        self.prev_after_cursor = self.after_cursor

    def reset_line(self, prompt):
        """Prepare for a new input line"""
        self.prompt = prompt
        self.before_cursor = ''
        self.after_cursor = ''
        self.reset_prev_line()

    def reset_prev_line(self):
        """Reset previous line (current line will repaint as new)"""
        self.prev_prompt = ''
        self.prev_before_cursor = ''
        self.prev_after_cursor = ''

    def changed(self):
        """Check whether a change has occurred in the input state (e.g. for repaint)"""
        return self.prompt != self.prev_prompt \
               or self.before_cursor != self.prev_before_cursor \
               or self.after_cursor != self.prev_after_cursor

    def handle(self, action, arg = None, arg2 = None):
        """Handle a keyboard action"""
        handler = self.handlers[action]
        if arg2 != None \
                and (action == ActionCode.ACTION_LEFT_WORD \
                or action == ActionCode.ACTION_RIGHT_WORD):
            handler(arg, arg2)
        elif arg != None \
                and (action in self.navigate_actions \
                    or action == ActionCode.ACTION_BACKSPACE_WORD \
                    or action == ActionCode.ACTION_DELETE_WORD) :
            # Navigation actions have a "select" argument
            handler(arg)
        elif action in self.insert_actions:
            # Insert actions have a "text" argument
            handler(arg)
        else:
            # Other actions don't have arguments
            handler()

        # Add the previous state as an undo state if needed
        if self.changed():
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO_EMACS:
                self.undo.append((self.prev_before_cursor, self.prev_after_cursor))
                self.redo = []
            if action in self.batch_actions \
                    or (action in self.insert_actions + self.delete_actions \
                            and action != self.last_action) \
                            or action == ActionCode.ACTION_UNDO:
                self.undo_emacs.append((self.prev_before_cursor, self.prev_after_cursor))
                self.undo_emacs_index = -1

        # print "\n", self.undo, "    ", self.redo, "\n"

        self.last_action = action


    def key_left(self, select=False):
        """
        Move cursor one position to the left
        Also handle text selection according to flag
        """
        if self.before_cursor != '':
            self.after_cursor = self.before_cursor[-1] + self.after_cursor
            self.before_cursor = self.before_cursor[0 : -1]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_right(self, select=False):
        """
        Move cursor one position to the right
        Also handle text selection according to flag
        """
        if self.after_cursor != '':
            self.before_cursor = self.before_cursor + self.after_cursor[0]
            self.after_cursor = self.after_cursor[1 : ]
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_home(self, select=False):
        """
        Home key
        Also handle text selection according to flag
        """
        self.after_cursor = self.before_cursor + self.after_cursor
        self.before_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()

    def key_end(self, select=False):
        """
        End key
        Also handle text selection according to flag
        """
        self.before_cursor = self.before_cursor + self.after_cursor
        self.after_cursor = ''
        if not select:
            self.reset_selection()
        self.history.reset()


    def key_left_word(self, select=False, sep=word_sep):
        """Move backward one word (Ctrl-Left)"""
        # Skip spaces
        while self.before_cursor != '' and self.before_cursor[-1] in  sep:
            self.key_left(select)

        # Jump over word
        while self.before_cursor != '' and not self.before_cursor[-1] in sep:
            self.key_left(select)

    def key_right_word(self, select=False, sep=word_sep):
        """Move forward one word (Ctrl-Right)"""
        # Skip spaces
        while self.after_cursor != '' and self.after_cursor[0] in sep:
            self.key_right(select)

        # Jump over word
        while self.after_cursor != '' and not self.after_cursor[0] in sep:
            self.key_right(select)

    def key_backspace_word(self, sep=word_sep):
        """Delte backwards one word (Ctrl-Left), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.before_cursor != '' and self.before_cursor[-1] in sep:
                self.key_backspace()

            # Jump over word
            while self.before_cursor != '' and not self.before_cursor[-1] in sep:
                self.key_backspace()

    def key_del_word(self, sep=word_sep):
        """Delete forwards one word (Ctrl-Right), or delete selection"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            # Skip spaces
            while self.after_cursor != '' and self.after_cursor[0] in sep:
                self.key_del()

            # Jump over word
            while self.after_cursor != '' and not self.after_cursor[0] in sep:
                self.key_del()
            
    def key_del(self):
        """Delete character at cursor"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = self.after_cursor[1 : ]
            self.history.reset()
            self.reset_selection()

    def key_kill_line(self):
        """Kill the rest of the current line"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.after_cursor = ''
        self.history.reset()

    def key_up(self):
        """Arrow up (history previous)"""

        # print '\n\n', history, history_index, '\n\n'
        if not self.history.trail:
            # Start search
            self.history.start(self.before_cursor + self.after_cursor)
        
        # don't update cursor and selection if there is no match in the command history
        if self.history.up() == True :
            # Clear undo/redo history
            # don't clear at undo/redo history at the start of key_up, since this up could be invalid
            self.undo = []
            self.redo = []
            
            self.before_cursor = self.history.current()[0]
            self.after_cursor = ''

        #print '\n\nHistory:', self.history
        #print 'Trail:', self.history_trail, '\n\n'

            self.reset_selection()

    def key_down(self):
        """Arrow down (history next)"""

        # Clear undo/redo history
        self.undo = []
        self.redo = []

        self.history.down()
        self.before_cursor = self.history.current()[0]
        self.after_cursor = ''

        self.reset_selection()

    def key_esc(self):
        """Esc key"""
        if self.get_selection() != '':
            # Reset selection, if any
            self.reset_selection()
        else:
            if self.history.filter != '':
                # Reset search filter, if any
                self.history.reset()
            # else:
            # clear the current line for ESC and reset history at the same time
            # Not need for consecutive 2 ESC to clear the console input
            # Clear current line (we keep it in the history though)
            # Don't add the current line to history if canceled (not run)
            #self.history.add(self.before_cursor + self.after_cursor)
            self.before_cursor = ''
            self.after_cursor = ''

    def key_backspace(self):
        """Backspace key"""
        if self.get_selection() != '':
            self.delete_selection()
        else:
            self.before_cursor = self.before_cursor[0 : -1]
            self.history.reset()
            self.reset_selection()

    def key_copy(self):
        """Copy selection to clipboard"""
        """Seems this copy/paste function is no longer needed"""
        """As copy/paste is supported natively by cmd.exe"""
        hwnd = ctypes.wintypes.HWND(0)
        self.user32_dll.OpenClipboard(hwnd);
        self.user32_dll.EmptyClipboard();
        self.user32_dll.SetClipboardData(1, self.get_selection()) # 1 is CF_TEXT
        self.user32_dll.CloseClipboard();
        self.history.reset()

    def key_cut(self):
        """Cut selection to clipboard"""
        self.key_copy()
        self.delete_selection()
        self.history.reset()

    def key_paste(self):
        """Paste from clipboard"""

        text = PyCmdUtils.GetClipboardText()
        if len(text) == 0:
            return
            
        # Purge garbage chars that some apps put in the clipboard
        if text.find(b'\0') >= 0:
            text = text[:text.find(b'\0')]

        # Convert newlines to blanks
        text = text.replace(b'\r', b'').replace(b'\n', b' ')

        # Insert into command line
        if self.get_selection() != '':
            self.delete_selection()
        self.before_cursor = self.before_cursor + text.decode()
        self.reset_selection()
        self.history.reset()

    def open_clip_board(self):
        """Pass clipboard content to %PYCMD_OPEN_APP%"""
        if len(self.open_app) == 0:
            return

        hwnd = ctypes.wintypes.HWND(0)
        self.user32_dll.OpenClipboard(hwnd);
        if self.user32_dll.IsClipboardFormatAvailable(1): #1 is CF_TEXT
            text = ''
            GetClipboardData = self.user32_dll.GetClipboardData
            GetClipboardData.argtypes = [ctypes.wintypes.UINT]
            GetClipboardData.restype = ctypes.wintypes.HANDLE
            pcontents = GetClipboardData(1)
            if pcontents:
                text = ctypes.c_char_p(pcontents).value.decode('utf-8')

            #Purge garbage chars that some apps put in the clipboard
            if text.find('\0') >= 0:
                text = text[:text.find('\0')]

            text = text.strip()
            expanded_text = os.path.expandvars(text)
            if os.path.isdir(expanded_text):
                os.system("explorer.exe " + expanded_text)
            elif len(text) > 0:
                import re
                # match error line output by MSVC for quick access.
                if match := re.match('^(\S+)\((\d+)\):$', text):
                    text = match[1] + '?' + match[2]
                os.system("cmd.exe /c" + self.open_app + " " + text)
        self.user32_dll.CloseClipboard();