def setUp(self): sublime.set_clipboard('') registers._REGISTER_DATA = {} TestsState.view.settings().erase('vintage') TestsState.view.settings().erase('vintageous_use_sys_clipboard') self.regs = Registers(view=TestsState.view, settings=SettingsManager(view=TestsState.view))
class VintageState(object): """ Stores per-view state using View.Settings() for storage. """ registers = Registers() context = KeyContext() marks = Marks() macros = {} # Let's imitate Sublime Text's .command_history() 'null' value. _latest_repeat_command = ('', None, 0) # Stores the latest recorded macro. _latest_macro = None _is_recording = False def __init__(self, view): self.view = view # We have two types of settings: vi-specific (settings.vi) and regular ST view settings # (settings.view). self.settings = SettingsManager(self.view) def enter_normal_mode(self): self.settings.view['command_mode'] = True self.settings.view['inverse_caret_state'] = True # Xpos must be updated every time we return to normal mode, because it doesn't get # updated while in insert mode. self.xpos = None if not self.view.sel() else self.view.rowcol( self.view.sel()[0].b)[1] if self.view.overwrite_status(): self.view.set_overwrite_status(False) # Clear regions outlined by buffer search commands. self.view.erase_regions('vi_search') if not self.buffer_was_changed_in_visual_mode(): # We've been in some visual mode, but we haven't modified the buffer at all. self.view.run_command('unmark_undo_groups_for_gluing') else: # Either we haven't been in any visual mode or we've modified the buffer while in # any visual mode. self.view.run_command('glue_marked_undo_groups') self.mode = MODE_NORMAL def enter_visual_line_mode(self): self.mode = MODE_VISUAL_LINE def enter_insert_mode(self): self.settings.view['command_mode'] = False self.settings.view['inverse_caret_state'] = False self.mode = MODE_INSERT def enter_visual_mode(self): self.mode = MODE_VISUAL def enter_normal_insert_mode(self): # This is the mode we enter when we give i a count, as in 5ifoobar<CR><ESC>. self.mode = MODE_NORMAL_INSERT self.settings.view['command_mode'] = False self.settings.view['inverse_caret_state'] = False def enter_replace_mode(self): self.mode = MODE_REPLACE self.settings.view['command_mode'] = False self.settings.view['inverse_caret_state'] = False self.view.set_overwrite_status(True) def store_visual_selections(self): self.view.add_regions('vi_visual_selections', list(self.view.sel())) def buffer_was_changed_in_visual_mode(self): """Returns `True` if we've changed the buffer while in visual mode. """ # XXX: What if we used view.is_dirty() instead? That should be simpler? # XXX: If we can be sure that every modifying command will leave the buffer in a dirty # state, we could go for this solution. # 'maybe_mark_undo_groups_for_gluing' and 'glue_marked_undo_groups' seem to add an entry # to the undo stack regardless of whether intervening modifying-commands have been # issued. # # Example: # 1) We enter visual mode by pressing 'v'. # 2) We exit visual mode by pressing 'v' again. # # Since before the first 'v' and after the second we've called the aforementioned commands, # respectively, we'd now have a new (useless) entry in the undo stack, and the redo stack # would be empty. This would be undesirable, so we need to find out whether marked groups # in visual mode actually need to be glued or not and act based on that information. # FIXME: Design issue. This method won't work always. We have actions like yy that # will make this method return true, while it should return False (since yy isn't a # modifying command). However, yy signals in its own way that it's a non-modifying command. # I don't think this redundancy will cause any bug, but we need to unify nevetheless. if self.mode == MODE_VISUAL: visual_cmd = 'vi_enter_visual_mode' elif self.mode == MODE_VISUAL_LINE: visual_cmd = 'vi_enter_visual_line_mode' else: return True cmds = [] # Set an upper limit to look-ups in the undo stack. for i in range(0, -249, -1): cmd_name, args, _ = self.view.command_history(i) if (cmd_name == 'vi_run' and args['action'] and args['action']['command'] == visual_cmd): break # Sublime Text returns ('', None, 0) when we hit the undo stack's bottom. if not cmd_name: break cmds.append((cmd_name, args)) # If we have an action between v..v calls (or visual line), we have modified the buffer # (most of the time, anyway, there are exceptions that we're not covering here). # TODO: Cover exceptions too, like yy (non-modifying command, though has the shape of a # modifying command). was_modifed = [ name for (name, data) in cmds if data and data.get('action') ] return bool(was_modifed) @property def mode(self): """The current mode. """ return self.settings.vi['mode'] @mode.setter def mode(self, value): self.settings.vi['mode'] = value @property def cancel_action(self): """Returns `True` if the current action must be cancelled. """ # If we can't find a suitable action, we should cancel. return self.settings.vi['cancel_action'] @cancel_action.setter def cancel_action(self, value): self.settings.vi['cancel_action'] = value @property def action(self): """Command's action; must be the name of a function in the `actions` module. """ return self.settings.vi['action'] @action.setter def action(self, name): action = self.settings.vi['action'] target = 'action' # Check for digraphs like cc, dd, yy. if action and name: name, type_ = digraphs.get((action, name), ('', None)) # Some motion digraphs are captured as actions, but need to be stored as motions # instead so that the vi command is evaluated correctly. if type_ == DIGRAPH_MOTION: target = 'motion' self.settings.vi['action'] = None # Avoid recursion. The .reset() method will try to set this property to None, not ''. if name == '': # The chord is invalid, so notify that we need to cancel the command in .eval(). self.cancel_action = True return self.settings.vi[target] = name @property def motion(self): """Command's motion; must be the name of a function in the `motions` module. """ return self.settings.vi['motion'] @motion.setter def motion(self, name): self.settings.vi['motion'] = name @property def motion_digits(self): """Count for the motion, like in 3k. """ return self.settings.vi['motion_digits'] or [] @motion_digits.setter def motion_digits(self, value): self.settings.vi['motion_digits'] = value def push_motion_digit(self, value): digits = self.settings.vi['motion_digits'] if not digits: self.settings.vi['motion_digits'] = [value] return digits.append(value) self.settings.vi['motion_digits'] = digits @property def action_digits(self): """Count for the action, as in 3dd. """ return self.settings.vi['action_digits'] or [] @action_digits.setter def action_digits(self, value): self.settings.vi['action_digits'] = value def push_action_digit(self, value): digits = self.settings.vi['action_digits'] if not digits: self.settings.vi['action_digits'] = [value] return digits.append(value) self.settings.vi['action_digits'] = digits @property def count(self): """Computes and returns the final count, defaulting to 1 if the user didn't provide one. """ motion_count = self.motion_digits and int(''.join( self.motion_digits)) or 1 action_count = self.action_digits and int(''.join( self.action_digits)) or 1 return (motion_count * action_count) @property def user_provided_count(self): """Returns the actual count provided by the user, which may be `None`. """ if not (self.motion_digits or self.action_digits): return None return self.count @property def expecting_register(self): """Signals that we need more input from the user before evaluating the global data. """ return self.settings.vi['expecting_register'] @expecting_register.setter def expecting_register(self, value): self.settings.vi['expecting_register'] = value @property def register(self): """Name of the register provided by the user, as in "ayy. """ return self.settings.vi['register'] or None @register.setter def register(self, name): # TODO: Check for valid register name. self.settings.vi['register'] = name self.expecting_register = False @property def expecting_user_input(self): """Signals that we need more input from the user before evaluating the global data. """ return self.settings.vi['expecting_user_input'] @expecting_user_input.setter def expecting_user_input(self, value): self.settings.vi['expecting_user_input'] = value @property def user_input(self): """Additional data provided by the user, as 'a' in @a. """ return self.settings.vi['user_input'] or None @user_input.setter def user_input(self, value): self.settings.vi['user_input'] = value self.expecting_user_input = False @property def last_buffer_search(self): """Returns the latest buffer search string or `None`. Used by the n and N commands. """ return self.settings.vi['last_buffer_search'] or None @last_buffer_search.setter def last_buffer_search(self, value): self.settings.vi['last_buffer_search'] = value @property def last_character_search(self): """Returns the latest character search or `None`. Used by the , and ; commands. """ return self.settings.vi['last_character_search'] or None @last_character_search.setter def last_character_search(self, value): # TODO: Should this piece of data be global instead of local to each buffer? self.settings.vi['last_character_search'] = value @property def xpos(self): """Maintains the current column for the caret in normal and visual mode. """ xpos = self.settings.vi['xpos'] return xpos if isinstance(xpos, int) else None @xpos.setter def xpos(self, value): self.settings.vi['xpos'] = value @property def next_mode(self): """Mode to transition to after the command has been run. For example, ce needs to change to insert mode after it's run. """ next_mode = self.settings.vi['next_mode'] or MODE_NORMAL return next_mode @next_mode.setter def next_mode(self, value): self.settings.vi['next_mode'] = value @property def next_mode_command(self): """Command to make the transitioning to the next mode. """ next_mode_command = self.settings.vi['next_mode_command'] return next_mode_command @next_mode_command.setter def next_mode_command(self, value): self.settings.vi['next_mode_command'] = value @property def repeat_command(self): """Latest modifying command performed. Accessed via '.'. """ # This property is volatile. It won't be persisted between sessions. return VintageState._latest_repeat_command @repeat_command.setter def repeat_command(self, value): VintageState._latest_repeat_command = value @property def latest_macro(self): """Latest macro recorded. Accessed via @@. """ return VintageState._latest_macro @latest_macro.setter def latest_macro(self, value): VintageState._latest_macro = value @property def is_recording(self): """Signals that we're recording a macro. """ return VintageState._is_recording @is_recording.setter def is_recording(self, value): VintageState._is_recording = value def parse_motion(self): """Returns a CmdData instance with parsed motion data. """ vi_cmd_data = CmdData(self) # This should happen only at initialization. # XXX: This is effectively zeroing xpos. Shouldn't we move this into new_vi_cmd_data()? # XXX: REFACTOR if vi_cmd_data['xpos'] is None: xpos = 0 if self.view.sel(): xpos = self.view.rowcol(self.view.sel()[0].b) self.xpos = xpos vi_cmd_data['xpos'] = xpos # Actions originating in normal mode are run in a pseudomode that helps to distiguish # between visual mode and this case (both use selections, either implicitly or # explicitly). if self.action and (self.mode == MODE_NORMAL): vi_cmd_data['mode'] = _MODE_INTERNAL_NORMAL motion = self.motion motion_func = None if motion: try: motion_func = getattr(motions, self.motion) except AttributeError: raise AttributeError( "Vintageous: Unknown motion: '{0}'".format(self.motion)) if motion_func: vi_cmd_data = motion_func(vi_cmd_data) return vi_cmd_data def parse_action(self, vi_cmd_data): """Updates and returns the passed-in CmdData instance using parsed data about the action. """ try: action_func = getattr(actions, self.action) except AttributeError: raise AttributeError("Vintageous: Unknown action: '{0}'".format( self.action)) except TypeError: raise TypeError( "Vintageous: parse_action requires an action be specified.") if action_func: vi_cmd_data = action_func(vi_cmd_data) # Notify global state to go ahead with the command if there are selections and the action # is ready to be run (which is almost always the case except for some digraphs). # NOTE: By virtue of checking for non-empty selections instead of an explicit mode, # the user can run actions on selections created outside of Vintageous. # This seems to work well. if (self.view.has_non_empty_selection_region() and # XXX: This check is pretty useless, because we abort early in .run() anyway. # Logically, it makes sense, however. not vi_cmd_data['is_digraph_start']): vi_cmd_data['motion_required'] = False return vi_cmd_data def eval_cancel_action(self): """Cancels the whole run of the command. """ # TODO: add a .parse() method that includes boths steps? vi_cmd_data = self.parse_motion() vi_cmd_data = self.parse_action(vi_cmd_data) if vi_cmd_data['must_blink_on_error']: utils.blink() # Modify the data that determines the mode we'll end up in when the command finishes. self.next_mode = vi_cmd_data['_exit_mode'] # Since we are exiting early, ensure we leave the selections as the commands wants them. if vi_cmd_data['_exit_mode_command']: self.view.run_command(vi_cmd_data['_exit_mode_command']) def eval_full_command(self): """Evaluates a command like 3dj, where there is an action as well as a motion. """ vi_cmd_data = self.parse_motion() vi_cmd_data = self.parse_action(vi_cmd_data) if not vi_cmd_data['is_digraph_start']: # We are about to run an action, so let Sublime Text know we want all editing # steps folded into a single sequence. "All editing steps" means slightly different # things depending on the mode we are in. if vi_cmd_data['_mark_groups_for_gluing']: self.view.run_command('maybe_mark_undo_groups_for_gluing') self.view.run_command('vi_run', vi_cmd_data) self.reset() else: # If we have a digraph start, the global data is in an invalid state because we # are still missing the complete digraph. Abort and clean up. if vi_cmd_data['_exit_mode'] == MODE_INSERT: # We've been requested to change to this mode. For example, we're looking at # CTRL+r,j in INSERTMODE, which is an invalid sequence. # !!! This could be simplified using parameters in .reset(), but then it # wouldn't be obvious what was going on. Don't refactor. !!! utils.blink() self.reset() self.enter_insert_mode() elif self.mode != MODE_NORMAL: # Normally we'd go back to normal mode. self.enter_normal_mode() self.reset() def eval_lone_action(self): """Evaluate lone action like in 'd' or 'esc'. Some actions can be run without a motion. """ vi_cmd_data = self.parse_motion() vi_cmd_data = self.parse_action(vi_cmd_data) if vi_cmd_data['is_digraph_start']: # XXX: When does this happen? Why are we only interested in MODE_NORMAL? # XXX In response to the above, this must be due to Ctrl+r. if vi_cmd_data['_change_mode_to'] == MODE_NORMAL: self.enter_normal_mode() # We know we are not ready. return if not vi_cmd_data['motion_required']: # We are about to run an action, so let Sublime Text know we want all editing # steps folded into a single sequence. "All editing steps" means slightly different # things depending on the mode we are in. if vi_cmd_data['_mark_groups_for_gluing']: self.view.run_command('maybe_mark_undo_groups_for_gluing') self.view.run_command('vi_run', vi_cmd_data) self.reset() self.update_status() # TODO: Test me. # TODO: Refactor so that .reset and update_status() are called in the separate methods. def eval(self): """Examines the current state and decides whether to actually run the action/motion. """ if self.cancel_action: self.eval_cancel_action() self.reset() # Action + motion, like in '3dj'. elif self.action and self.motion: self.eval_full_command() # Motion only, like in '3j'. elif self.motion: vi_cmd_data = self.parse_motion() self.view.run_command('vi_run', vi_cmd_data) self.reset() self.update_status() # Action only, like in 'd' or 'esc'. Some actions can be executed without a motion. elif self.action: self.eval_lone_action() def reset(self): """Reset global state. """ had_action = self.action self.motion = None self.action = None self.register = None self.user_input = None self.expecting_register = False self.expecting_user_input = False self.cancel_action = False # In MODE_NORMAL_INSERT, we temporarily exit NORMAL mode, but when we get back to # it, we need to know the repeat digits, so keep them. An example command for this case # would be 5ifoobar\n<esc> starting in NORMAL mode. if self.mode == MODE_NORMAL_INSERT: return self.motion_digits = [] self.action_digits = [] if self.next_mode in (MODE_NORMAL, MODE_INSERT): if self.next_mode_command: self.view.run_command(self.next_mode_command) # Sometimes we'll reach this point after performing motions. If we have a stored repeat # command in view A, we switch to view B and do a motion, we don't want .update_repeat_command() # to inspect view B's undo stack and grab its latest modifying command; we want to keep # view A's instead, which is what's stored in _latest_repeat_command. We only want to # update this when there is a new action. # FIXME: Even that will fail when we perform an action that does not modify the buffer, # like splitting the window. The current view's latest modifying command will overwrite # the genuine _latest_repeat_command. The correct solution seems to be to tag every single # modifying command with a 'must_update_repeat_command' attribute. if had_action: self.update_repeat_command() self.next_mode = MODE_NORMAL self.next_mode_command = None def update_repeat_command(self): """Vintageous manages the repeat command on its own. Vim stores away the latest modifying command as the repeat command, and does not wipe it when undoing. On the contrary, Sublime Text will update the repeat command as soon as you undo past the current one. The then previous latest modifying command becomes the new repeat command, and so on. """ cmd, args, times = self.view.command_history(0, True) if not cmd: return elif cmd == 'vi_run' and args.get('action'): self.repeat_command = cmd, args, times elif cmd == 'sequence': # XXX: We are assuming every 'sequence' command is a modifying command, which seems # to be reasonable, but I dunno. self.repeat_command = cmd, args, times elif cmd != 'vi_run': # XXX: We are assuming every 'native' command is a modifying commmand, but it doesn't # feel right... self.repeat_command = cmd, args, times # TODO: Test me. def update_xpos(self): first_sel = self.view.sel()[0] xpos = 0 if self.mode == MODE_VISUAL: if first_sel.a < first_sel.b: xpos = self.view.rowcol(first_sel.b - 1)[1] elif first_sel.a > first_sel.b: xpos = self.view.rowcol(first_sel.b)[1] elif self.mode == MODE_NORMAL: xpos = self.view.rowcol(first_sel.b)[1] self.xpos = xpos # TODO: Test me. def update_status(self): """Print to Sublime Text's status bar. """ mode_name = mode_to_str(self.mode) or "" mode_name = "-- %s --" % mode_name if mode_name else "" sublime.status_message(mode_name)
class State(object): """ Manages global Vim state. Accumulates command data, etc. Usage: Always instantiate passing it the view that commands are going to target. Example: state = State(view) Notes: `State` internally uses view.settings() and window.settings() to persist data. """ registers = Registers() macro_registers = MacroRegisters() marks = Marks() context = KeyContext() variables = Variables() macro_steps = [] def __init__(self, view): self.view = view # We use several types of settings: # - vi-specific (settings.vi), # - regular ST view settings (settings.view) and # - window settings (settings.window). self.settings = SettingsManager(self.view) _logger().debug( '[State] Is .view an ST/Vintageous widget? {0}/{1}'.format( bool(self.settings.view['is_widget']), bool(self.settings.view['is_vintageous_widget']))) @property def glue_until_normal_mode(self): """ Indicates that editing commands should be grouped together in a single undo step after the user requested `_enter_normal_mode` next. This property is *VOLATILE*; it shouldn't be persisted between sessions. """ return self.settings.vi['_vintageous_glue_until_normal_mode'] or False @glue_until_normal_mode.setter def glue_until_normal_mode(self, value): self.settings.vi['_vintageous_glue_until_normal_mode'] = value @property def processing_notation(self): """ Indicates whether `ProcessNotation` is running a command and is grouping all edits in one single undo step. That is, we are running a non-interactive sequence of commands. This property is *VOLATILE*; it shouldn't be persisted between sessions. """ # TODO(guillermooo): Rename self.settings.vi to self.settings.local return self.settings.vi['_vintageous_processing_notation'] or False @processing_notation.setter def processing_notation(self, value): self.settings.vi['_vintageous_processing_notation'] = value # FIXME: This property seems to do the same as processing_notation. @property def non_interactive(self): """ Indicates whether `ProcessNotation` is running a command and no interactive prompts should be used (for example, by the '/' motion.) This property is *VOLATILE*; it shouldn't be persisted between sessions. """ return self.settings.vi['_vintageous_non_interactive'] or False @non_interactive.setter def non_interactive(self, value): assert isinstance(value, bool), 'bool expected' self.settings.vi['_vintageous_non_interactive'] = value @property def last_character_search(self): """ Last character used as input for 'f' or 't'. """ return self.settings.window['_vintageous_last_character_search'] or '' @last_character_search.setter def last_character_search(self, value): assert isinstance(value, str), 'bad call' assert len(value) == 1, 'bad call' self.settings.window['_vintageous_last_character_search'] = value @property def last_char_search_command(self): """ ',' and ';' change directions depending on which of 'f' or 't' was the previous command. Returns the name of the last character search command, namely: 'vi_f', 'vi_t', 'vi_big_f' or 'vi_big_t'. """ name = self.settings.window['_vintageous_last_char_search_command'] return name or 'vi_f' @last_char_search_command.setter def last_char_search_command(self, value): self.settings.window['_vintageous_last_char_search_command'] = value @property def last_buffer_search_command(self): """ 'n' and 'N' change directions depending on which of '/' or '?' was the previous command. Returns the name of the last character search command, namely: 'vi_slash', 'vi_question_mark', 'vi_star', 'vi_octothorp' """ name = self.settings.window['_vintageous_last_buffer_search_command'] return name or 'vi_slash' @last_buffer_search_command.setter def last_buffer_search_command(self, value): self.settings.window['_vintageous_last_buffer_search_command'] = value @property def must_capture_register_name(self): """ Returns `True` if `State` is expecting a register name next. """ return self.settings.vi['must_capture_register_name'] or False @must_capture_register_name.setter def must_capture_register_name(self, value): self.settings.vi['must_capture_register_name'] = value @property def last_buffer_search(self): """ Returns the last string used by buffer search commands '/' or '?'. """ return self.settings.window['_vintageous_last_buffer_search'] or '' @last_buffer_search.setter def last_buffer_search(self, value): self.settings.window['_vintageous_last_buffer_search'] = value @property def reset_during_init(self): # Some commands gather user input through input panels. An input panel # is just a view, so when it's closed, the previous view gets # activated and Vintageous init code runs. In this case, however, we # most likely want the global state to remain unchanged. This variable # helps to signal this. # # For an example, see the '_vi_slash' command. value = self.settings.window['_vintageous_reset_during_init'] if not isinstance(value, bool): return True return value @reset_during_init.setter def reset_during_init(self, value): assert isinstance(value, bool), 'expected a bool' self.settings.window['_vintageous_reset_during_init'] = value # This property isn't reset automatically. _enter_normal_mode mode must # take care of that so it can repeat the commands issued while in # insert mode. @property def normal_insert_count(self): """ Count issued to 'i' or 'a', etc. These commands enter insert mode. If passed a count, they must repeat the commands run while in insert mode. """ return self.settings.vi['normal_insert_count'] or '1' @normal_insert_count.setter def normal_insert_count(self, value): self.settings.vi['normal_insert_count'] = value @property def sequence(self): """ Sequence of keys provided by the user. """ return self.settings.vi['sequence'] or '' @sequence.setter def sequence(self, value): self.settings.vi['sequence'] = value @property def partial_sequence(self): """ Sometimes we need to store a partial sequence to obtain the commands' full name. Such is the case of `gD`, for example. """ return self.settings.vi['partial_sequence'] or '' @partial_sequence.setter def partial_sequence(self, value): self.settings.vi['partial_sequence'] = value @property def mode(self): """ Current mode. It isn't guaranteed that the underlying view's .sel() will be in a consistent state (for example, that it will at least have one non-empty region in visual mode. """ return self.settings.vi['mode'] or modes.UNKNOWN @mode.setter def mode(self, value): self.settings.vi['mode'] = value @property def action(self): action = self.settings.vi['action'] or None if action: cls = getattr(cmd_defs, action['name'], None) if cls is None: cls = user_plugins.classes.get(action['name'], None) if cls is None: ValueError('unknown action: %s' % action) return cls.from_json(action['data']) @action.setter def action(self, value): serialized = value.serialize() if value else None self.settings.vi['action'] = serialized @property def motion(self): motion = self.settings.vi['motion'] or None if motion: cls = getattr(cmd_defs, motion['name']) return cls.from_json(motion['data']) @motion.setter def motion(self, value): serialized = value.serialize() if value else None self.settings.vi['motion'] = serialized @property def motion_count(self): return self.settings.vi['motion_count'] or '' @motion_count.setter def motion_count(self, value): assert value == '' or value.isdigit(), 'bad call' self.settings.vi['motion_count'] = value @property def action_count(self): return self.settings.vi['action_count'] or '' @action_count.setter def action_count(self, value): assert value == '' or value.isdigit(), 'bad call' self.settings.vi['action_count'] = value @property @settings.volatile def repeat_data(self): """ Stores (type, cmd_name_or_key_seq, , mode) for '.' to use. `type` may be 'vi' or 'native'. `vi`-commands are executed via `ProcessNotation`, while `native`-commands are executed via .run_command(). """ return self.settings.vi['repeat_data'] or None @repeat_data.setter def repeat_data(self, value): assert isinstance(value, tuple) or isinstance(value, list), 'bad call' assert len(value) == 4, 'bad call' self.logger.info("setting repeat data {0}".format(value)) self.settings.vi['repeat_data'] = value @property def count(self): """ Calculates the actual count for the current command. """ c = 1 if self.action_count: c = int(self.action_count) or 1 if self.motion_count: c *= (int(self.motion_count) or 1) if c < 1: raise ValueError('count must be positive') return c @property def xpos(self): """ Stores the current xpos for carets. """ return self.settings.vi['xpos'] or 0 @xpos.setter def xpos(self, value): assert isinstance(value, int), '`value` must be an int' self.settings.vi['xpos'] = value @property def visual_block_direction(self): """ Stores the visual block direction for the current selection. """ return self.settings.vi['visual_block_direction'] or directions.DOWN @visual_block_direction.setter def visual_block_direction(self, value): assert isinstance(value, int), '`value` must be an int' self.settings.vi['visual_block_direction'] = value # FIXME(guillermooo): Remove this and use a global logger. @property def logger(self): global _logger return _logger() @property def register(self): """ Stores the current open register, as requested by the user. """ return self.settings.vi['register'] or '"' @register.setter def register(self, value): assert len(str(value)) == 1, '`value` must be a character' self.logger.info('opening register {0}'.format(value)) self.settings.vi['register'] = value self.must_capture_register_name = False @property def must_collect_input(self): """ Returns `True` if state must collect input for the current motion or operator. """ if self.motion and self.action: if self.motion.accept_input: return True return (self.action.accept_input and self.action.input_parser.type == input_types.AFTER_MOTION) # Special case: `q` should stop the macro recorder if it's running and # not request further input from the user. if (isinstance(self.action, cmd_defs.ViToggleMacroRecorder) and self.is_recording): return False if (self.action and self.action.accept_input and self.action.input_parser.type == input_types.INMEDIATE): return True if self.motion: return (self.motion and self.motion.accept_input) @property def must_update_xpos(self): if self.motion and self.motion.updates_xpos: return True if self.action and self.action.updates_xpos: return True @property def is_recording(self): return self.settings.vi['recording'] or False @is_recording.setter def is_recording(self, value): assert isinstance(value, bool), 'bad call' self.settings.vi['recording'] = value def enter_normal_mode(self): self.mode = modes.NORMAL def enter_visual_mode(self): self.mode = modes.VISUAL def enter_visual_line_mode(self): self.mode = modes.VISUAL_LINE def enter_insert_mode(self): self.mode = modes.INSERT def enter_replace_mode(self): self.mode = modes.REPLACE def enter_select_mode(self): self.mode = modes.SELECT def enter_visual_block_mode(self): self.mode = modes.VISUAL_BLOCK def reset_sequence(self): # TODO(guillermooo): When is_recording, we could store the .sequence # and replay that, but we can't easily translate key presses in insert # mode to a Vintageous-friendly notation. A hybrid approach may work: # use a plain string for any command-mode-based mode, and native ST # commands for insert mode. That should make editing macros easier. self.sequence = '' def reset_partial_sequence(self): self.partial_sequence = '' def reset_register_data(self): self.register = '"' self.must_capture_register_name = False def reset_status(self): self.view.erase_status('vim-seq') if self.mode == modes.NORMAL: self.view.erase_status('vim-mode') def display_status(self): msg = "{0} {1}" mode_name = modes.to_friendly_name(self.mode) if mode_name: mode_name = '-- {0} --'.format(mode_name) if mode_name else '' self.view.set_status('vim-mode', mode_name) self.view.set_status('vim-seq', self.sequence) def must_scroll_into_view(self): return ((self.motion and self.motion.scroll_into_view) or (self.action and self.action.scroll_into_view)) def scroll_into_view(self): v = sublime.active_window().active_view() # TODO(guillermooo): Maybe some commands should show their # surroundings too? # Make sure we show the first caret on the screen, but don't show # its surroundings. v.show(v.sel()[0], False) def reset(self): # TODO: Remove this when we've ported all commands. This is here for # retrocompatibility. self.reset_command_data() def reset_command_data(self): # Resets all temporary data needed to build a command or partial # command. self.update_xpos() if self.must_scroll_into_view(): self.scroll_into_view() self.action and self.action.reset() self.action = None self.motion and self.motion.reset() self.motion = None self.action_count = '' self.motion_count = '' self.reset_sequence() self.reset_partial_sequence() self.reset_register_data() self.reset_status() def reset_volatile_data(self): """ Resets window- or application-wide data to their default values when starting a new Vintageous session. """ self.glue_until_normal_mode = False self.view.run_command('unmark_undo_groups_for_gluing') self.processing_notation = False self.non_interactive = False self.reset_during_init = True def update_xpos(self, force=False): if self.must_update_xpos or force: try: # TODO: we should check the current mode instead. ============ sel = self.view.sel()[0] pos = sel.b if not sel.empty(): if sel.a < sel.b: pos -= 1 # ============================================================ r = sublime.Region(self.view.line(pos).a, pos) counter = Counter(self.view.substr(r)) tab_size = self.view.settings().get('tab_size') xpos = (self.view.rowcol(pos)[1] + ((counter['\t'] * tab_size) - counter['\t'])) except Exception as e: print(e) _logger.error( 'Vintageous: Error when setting xpos. Defaulting to 0.') _logger.show_error(e) self.xpos = 0 return else: self.xpos = xpos def _set_parsers(self, command): """ Returns `True` if we've had to run an immediate parser via an input panel. """ if command.accept_input: return self._run_parser_via_panel(command) def _run_parser_via_panel(self, command): """ Returns `True` if the current parser needs to be run via a panel. If needed, it runs the input-panel-based parser. """ if command.input_parser.type == input_types.VIA_PANEL: if self.non_interactive: return False sublime.active_window().run_command(command.input_parser.command) return True return False def process_user_input2(self, key): assert self.must_collect_input, "call only if input is required" _logger().info('[State] processing input {0}'.format(key)) if self.motion and self.motion.accept_input: motion = self.motion val = motion.accept(key) self.motion = motion return val action = self.action val = action.accept(key) self.action = action return val def set_command(self, command): """ Sets the current command to @command. @command A command definition as found in `keys.py`. """ assert isinstance(command, cmd_base.ViCommandDefBase), \ 'ViCommandDefBase expected, got {0}'.format(type(command)) if isinstance(command, cmd_base.ViMotionDef): if self.runnable(): # We already have a motion, so this looks like an error. raise ValueError('too many motions') self.motion = command if self.mode == modes.OPERATOR_PENDING: self.mode = modes.NORMAL if self._set_parsers(command): return elif isinstance(command, cmd_base.ViOperatorDef): if self.runnable(): # We already have an action, so this looks like an error. raise ValueError('too many actions') self.action = command if (self.action.motion_required and not self.in_any_visual_mode()): self.mode = modes.OPERATOR_PENDING if self._set_parsers(command): return else: self.logger.info("[State] command: {0}".format(command)) raise ValueError('unexpected command type') def in_any_visual_mode(self): return (self.mode in (modes.VISUAL, modes.VISUAL_LINE, modes.VISUAL_BLOCK)) def can_run_action(self): if (self.action and (not self.action['motion_required'] or self.in_any_visual_mode())): return True def get_visual_repeat_data(self): """Returns the data needed to restore visual selections before repeating a visual mode command in normal mode. """ if self.mode not in (modes.VISUAL, modes.VISUAL_LINE): return first = first_sel(self.view) lines = (utils.row_at(self.view, first.end()) - utils.row_at(self.view, first.begin())) if lines > 0: chars = utils.col_at(self.view, first.end()) else: chars = first.size() return (lines, chars, self.mode) def restore_visual_data(self, data): rows, chars, old_mode = data first = first_sel(self.view) if old_mode == modes.VISUAL: if rows > 0: end = self.view.text_point( utils.row_at(self.view, first.b) + rows, chars) else: end = first.b + chars self.view.sel().add(sublime.Region(first.b, end)) self.mode = modes.VISUAL elif old_mode == modes.VISUAL_LINE: rows, _, old_mode = data begin = self.view.line(first.b).a end = self.view.text_point( utils.row_at(self.view, begin) + (rows - 1), 0) end = self.view.full_line(end).b self.view.sel().add(sublime.Region(begin, end)) self.mode = modes.VISUAL_LINE def start_recording(self): self.is_recording = True State.macro_steps = [] self.view.set_status('vim-recorder', 'Recording...') def stop_recording(self): self.is_recording = False self.view.erase_status('vim-recorder') def add_macro_step(self, cmd_name, args): if self.is_recording: if cmd_name == '_vi_q': # don't store the ending macro step return if self.runnable and not self.glue_until_normal_mode: State.macro_steps.append((cmd_name, args)) def runnable(self): """ Returns `True` if we can run the state data as it is. """ if self.must_collect_input: return False if self.action and self.motion: if self.mode != modes.NORMAL: raise ValueError('wrong mode') return True if self.can_run_action(): if self.mode == modes.OPERATOR_PENDING: raise ValueError('wrong mode') return True if self.motion: if self.mode == modes.OPERATOR_PENDING: raise ValueError('wrong mode') return True return False def eval(self): """ Run data as a command if possible. """ if not self.runnable(): return if self.action and self.motion: action_cmd = self.action.translate(self) motion_cmd = self.motion.translate(self) self.logger.info( '[State] full command, switching to internal normal mode') self.mode = modes.INTERNAL_NORMAL # TODO: Make a requirement that motions and actions take a # 'mode' param. if 'mode' in action_cmd['action_args']: action_cmd['action_args']['mode'] = modes.INTERNAL_NORMAL if 'mode' in motion_cmd['motion_args']: motion_cmd['motion_args']['mode'] = modes.INTERNAL_NORMAL args = action_cmd['action_args'] args['count'] = 1 # let the action run the motion within its edit object so that # we don't need to worry about grouping edits to the buffer. args['motion'] = motion_cmd self.logger.info( '[Stage] motion in motion+action: {0}'.format(motion_cmd)) if self.glue_until_normal_mode and not self.processing_notation: # We need to tell Sublime Text now that it should group # all the next edits until we enter normal mode again. sublime.active_window().run_command( 'mark_undo_groups_for_gluing') self.add_macro_step(action_cmd['action'], args) sublime.active_window().run_command(action_cmd['action'], args) if not self.non_interactive: if self.action.repeatable: self.repeat_data = ('vi', str(self.sequence), self.mode, None) self.reset_command_data() return if self.motion: motion_cmd = self.motion.translate(self) self.logger.info('[State] lone motion cmd: {0}'.format(motion_cmd)) self.add_macro_step(motion_cmd['motion'], motion_cmd['motion_args']) # We know that all motions are subclasses of ViTextCommandBase, # so it's safe to call them from the current view. self.view.run_command(motion_cmd['motion'], motion_cmd['motion_args']) if self.action: action_cmd = self.action.translate(self) self.logger.info('[Stage] lone action cmd '.format(action_cmd)) if self.mode == modes.NORMAL: self.logger.info('[State] switching to internal normal mode') self.mode = modes.INTERNAL_NORMAL if 'mode' in action_cmd['action_args']: action_cmd['action_args']['mode'] = \ modes.INTERNAL_NORMAL elif self.mode in (modes.VISUAL, modes.VISUAL_LINE): self.view.add_regions('visual_sel', list(self.view.sel())) # Some commands, like 'i' or 'a', open a series of edits that # need to be grouped together unless we are gluing a larger # sequence through ProcessNotation. For example, aFOOBAR<Esc> should # be grouped atomically, but not inside a sequence like # iXXX<Esc>llaYYY<Esc>, where we want to group the whole # sequence instead. if self.glue_until_normal_mode and not self.processing_notation: sublime.active_window().run_command( 'mark_undo_groups_for_gluing') seq = self.sequence visual_repeat_data = self.get_visual_repeat_data() action = self.action self.add_macro_step(action_cmd['action'], action_cmd['action_args']) sublime.active_window().run_command(action_cmd['action'], action_cmd['action_args']) if not (self.processing_notation and self.glue_until_normal_mode): if action.repeatable: self.repeat_data = ('vi', seq, self.mode, visual_repeat_data) self.logger.info('running command: action: {0} motion: {1}'.format( self.action, self.motion)) if self.mode == modes.INTERNAL_NORMAL: self.enter_normal_mode() self.reset_command_data()
class TestCaseRegisters(unittest.TestCase): def setUp(self): sublime.set_clipboard('') registers._REGISTER_DATA = {} TestsState.view.settings().erase('vintage') TestsState.view.settings().erase('vintageous_use_sys_clipboard') self.regs = Registers(view=TestsState.view, settings=SettingsManager(view=TestsState.view)) def testCanInitializeClass(self): self.assertEqual(self.regs.view, TestsState.view) self.assertTrue(getattr(self.regs, 'settings')) def testCanSetUnanmedRegister(self): self.regs._set_default_register(["foo"]) self.assertEqual(registers._REGISTER_DATA[registers.REG_UNNAMED], ["foo"]) def testSettingLongRegisterNameThrowsAssertionError(self): self.assertRaises(AssertionError, self.regs.set, "aa", "foo") def testSettingNonListValueThrowsAssertionError(self): self.assertRaises(AssertionError, self.regs.set, "a", "foo") @unittest.skip("Not implemented.") def testUnknownRegisterNameThrowsException(self): # XXX Doesn't pass at the moment. self.assertRaises(Exception, self.regs.set, "~", "foo") def testRegisterDataIsAlwaysStoredAsString(self): self.regs.set('"', [100]) self.assertEqual(registers._REGISTER_DATA[registers.REG_UNNAMED], ["100"]) def testSettingBlackHoleRegisterDoesNothing(self): registers._REGISTER_DATA[registers.REG_UNNAMED] = ["bar"] # In this case it doesn't matter whether we're setting a list or not, # because we are discarding the value anyway. self.regs.set(registers.REG_BLACK_HOLE, "foo") self.assertTrue(registers.REG_BLACK_HOLE not in registers._REGISTER_DATA) self.assertTrue(registers._REGISTER_DATA[registers.REG_UNNAMED], ["bar"]) def testSettingExpressionRegisterDoesntPopulateUnnamedRegister(self): self.regs.set("=", [100]) self.assertTrue(registers.REG_UNNAMED not in registers._REGISTER_DATA) self.assertEqual(registers._REGISTER_DATA[registers.REG_EXPRESSION], ["100"]) def testCanSetNormalRegisters(self): for name in registers.REG_VALID_NAMES: self.regs.set(name, [name]) for number in registers.REG_VALID_NUMBERS: self.regs.set(number, [number]) for name in registers.REG_VALID_NAMES: self.assertEqual(registers._REGISTER_DATA[name], [name]) for number in registers.REG_VALID_NUMBERS: self.assertEqual(registers._REGISTER_DATA[number], [number]) def testSettingNormalRegisterSetsUnnamedRegisterToo(self): self.regs.set('a', [100]) self.assertEqual(registers._REGISTER_DATA[registers.REG_UNNAMED], ['100']) self.regs.set('0', [200]) self.assertEqual(registers._REGISTER_DATA[registers.REG_UNNAMED], ['200']) def testSettingRegisterSetsClipboardIfNeeded(self): self.regs.settings.view['vintageous_use_sys_clipboard'] = True self.regs.set('a', [100]) self.assertEqual(sublime.get_clipboard(), '100') def testCanAppendToSingleValue(self): self.regs.set('a', ['foo']) self.regs.append_to('A', ['bar']) self.assertEqual(registers._REGISTER_DATA['a'], ['foobar']) def testCanAppendToMultipleBalancedValues(self): self.regs.set('a', ['foo', 'bar']) self.regs.append_to('A', ['fizz', 'buzz']) self.assertEqual(registers._REGISTER_DATA['a'], ['foofizz', 'barbuzz']) def testCanAppendToMultipleValuesMoreExistingValues(self): self.regs.set('a', ['foo', 'bar']) self.regs.append_to('A', ['fizz']) self.assertEqual(registers._REGISTER_DATA['a'], ['foofizz', 'bar']) def testCanAppendToMultipleValuesMoreNewValues(self): self.regs.set('a', ['foo']) self.regs.append_to('A', ['fizz', 'buzz']) self.assertEqual(registers._REGISTER_DATA['a'], ['foofizz', 'buzz']) def testAppendingSetsDefaultRegister(self): self.regs.set('a', ['foo']) self.regs.append_to('A', ['bar']) self.assertEqual(registers._REGISTER_DATA[registers.REG_UNNAMED], ['foobar']) def testAppendSetsClipboardIfNeeded(self): self.regs.settings.view['vintageous_use_sys_clipboard'] = True self.regs.set('a', ['foo']) self.regs.append_to('A', ['bar']) self.assertEqual(sublime.get_clipboard(), 'foobar') def testGetDefaultToUnnamedRegister(self): registers._REGISTER_DATA['"'] = ['foo'] self.assertEqual(self.regs.get(), ['foo']) def testGettingBlackHoleRegisterReturnsNone(self): self.assertEqual(self.regs.get(registers.REG_BLACK_HOLE), None) def testCanGetFileNameRegister(self): fname = self.regs.get(registers.REG_FILE_NAME) self.assertEqual(fname, [TestsState.view.file_name()]) def testCanGetClipboardRegisters(self): self.regs.set(registers.REG_SYS_CLIPBOARD_1, ['foo']) self.assertEqual(self.regs.get(registers.REG_SYS_CLIPBOARD_1), ['foo']) self.assertEqual(self.regs.get(registers.REG_SYS_CLIPBOARD_2), ['foo']) def testGetSysClipboardAlwaysIfRequested(self): self.regs.settings.view['vintageous_use_sys_clipboard'] = True sublime.set_clipboard('foo') self.assertEqual(self.regs.get(), ['foo']) def testGettingExpressionRegisterClearsExpressionRegister(self): registers._REGISTER_DATA[registers.REG_EXPRESSION] = ['100'] self.assertEqual(self.regs.get(), ['100']) self.assertEqual(registers._REGISTER_DATA[registers.REG_EXPRESSION], '') def testCanGetNumberRegister(self): registers._REGISTER_DATA['5'] = ['foo'] self.assertEqual(self.regs.get('5'), ['foo']) def testCanGetRegisterEvenIfRequestingItThroughACapitalLetter(self): registers._REGISTER_DATA['a'] = ['foo'] self.assertEqual(self.regs.get('A'), ['foo']) def testCanGetRegistersWithDictSyntax(self): registers._REGISTER_DATA['a'] = ['foo'] self.assertEqual(self.regs.get('a'), self.regs['a']) def testCanSetRegistersWithDictSyntax(self): self.regs['a'] = ['100'] self.assertEqual(self.regs['a'], ['100']) def testCanAppendToRegisteWithDictSyntax(self): self.regs['a'] = ['100'] self.regs['A'] = ['100'] self.assertEqual(self.regs['a'], ['100100']) def testCanConvertToDict(self): self.regs['a'] = ['100'] self.regs['b'] = ['200'] values = {name: self.regs.get(name) for name in registers.REG_ALL} values.update({'a': ['100'], 'b': ['200']}) self.assertEqual(self.regs.to_dict(), values) def testGettingEmptyRegisterReturnsNone(self): self.assertEqual(self.regs.get('a'), None)
class State(object): """ Manages global state needed to build commands and control modes, etc. Usage: Before using it, always instantiate with the view commands are going to target. `State` uses view.settings() and window.settings() for data storage. """ registers = Registers() marks = Marks() context = KeyContext() def __init__(self, view): self.view = view # We have multiple types of settings: vi-specific (settings.vi) and # regular ST view settings (settings.view) and window settings # (settings.window). # TODO: Make this a descriptor. Why isn't it? self.settings = SettingsManager(self.view) _logger().info( '[State] is .view an ST:Vintageous widget: {0}:{1}'.format( bool(self.settings.view['is_widget']), bool(self.settings.view['is_vintageous_widget']))) @property def glue_until_normal_mode(self): """ Indicates that editing commands should be grouped together in a single undo step once the user requests `_enter_normal_mode`. This property is *VOLATILE*; it shouldn't be persisted between sessions. """ # FIXME: What happens when we have an incomplete command and we switch # views? We should clean up. # TODO: Make this a window setting. return self.settings.vi['_vintageous_glue_until_normal_mode'] or False @glue_until_normal_mode.setter def glue_until_normal_mode(self, value): self.settings.vi['_vintageous_glue_until_normal_mode'] = value @property def gluing_sequence(self): """ Indicates whether `PressKeys` is running a command and is grouping all of the edits in one single undo step. This property is *VOLATILE*; it shouldn't be persisted between sessions. """ # TODO: Store this as a window setting. return self.settings.vi['_vintageous_gluing_sequence'] or False @gluing_sequence.setter def gluing_sequence(self, value): self.settings.vi['_vintageous_gluing_sequence'] = value @property def non_interactive(self): # FIXME: This property seems to do the same as gluing_sequence. """ Indicates whether `PressKeys` is running a command and no interactive prompts should be used (for example, by the '/' motion.) This property is *VOLATILE*; it shouldn't be persisted between sessions. """ # TODO: Store this as a window setting. return self.settings.vi['_vintageous_non_interactive'] or False @non_interactive.setter def non_interactive(self, value): if not isinstance(value, bool): raise ValueError('expected bool') self.settings.vi['_vintageous_non_interactive'] = value @property def input_parsers(self): """ Contains parsers for user input, like '/' or 'f' need. """ # TODO: Rename to 'validators'? # TODO: Use window settings for storage? # TODO: Enable setting lists directly. return self.settings.vi['_vintageous_input_parsers'] or [] @input_parsers.setter def input_parsers(self, value): self.settings.vi['_vintageous_input_parsers'] = value @property def last_character_search(self): """ Last character used as input for 'f' or 't'. """ return self.settings.window['_vintageous_last_character_search'] or '' @last_character_search.setter def last_character_search(self, value): self.settings.window['_vintageous_last_character_search'] = value @property def last_character_search_forward(self): """ ',' and ';' change directions depending on whether 'f' or 't' was issued previously. Returns the name of the last character search command, namely one of: vi_f, vi_t, vi_big_f, vi_big_t. """ return self.settings.window[ '_vintageous_last_character_search_forward'] or 'vi_f' @last_character_search_forward.setter def last_character_search_forward(self, value): # FIXME: It isn't working. self.settings.window[ '_vintageous_last_character_search_forward'] = value @property def capture_register(self): """ Returns `True` if `State` is expecting a register name next. """ return self.settings.vi['capture_register'] or False @capture_register.setter def capture_register(self, value): self.settings.vi['capture_register'] = value @property def last_buffer_search(self): """ Returns the last string used by buffer search commands such as '/' and '?'. """ return self.settings.window['_vintageous_last_buffer_search'] or '' @last_buffer_search.setter def last_buffer_search(self, value): self.settings.window['_vintageous_last_buffer_search'] = value @property def reset_during_init(self): # Some commands gather user input through input panels. An input panel # is just a view, so when it's closed, the previous view gets # activated and Vintageous init code runs. In this case, however, we # most likely want the global state to remain unchanged. This variable # helps to signal this. # # For an example, see the '_vi_slash' command. value = self.settings.window['_vintageous_reset_during_init'] if not isinstance(value, bool): return True return value @reset_during_init.setter def reset_during_init(self, value): if not isinstance(value, bool): raise ValueError('expected a bool') self.settings.window['_vintageous_reset_during_init'] = value # This property isn't reset automatically. _enter_normal_mode mode must # take care of that so it can repeat the commands issues while in # insert mode. @property def normal_insert_count(self): """ Count issued to 'i' or 'a', etc. These commands enter insert mode. If passed a count, they must repeat the commands issued while in insert mode. """ return self.settings.vi['normal_insert_count'] or '1' @normal_insert_count.setter def normal_insert_count(self, value): self.settings.vi['normal_insert_count'] = value # TODO: Make these simple properties that access settings descriptors? @property def sequence(self): """ Sequence of keys that build the command. """ return self.settings.vi['sequence'] or '' @sequence.setter def sequence(self, value): self.settings.vi['sequence'] = value @property def partial_sequence(self): """ Sometimes we need to store a partial sequence to obtain the commands' full name. Such is the case of `gD`, for example. """ return self.settings.vi['partial_sequence'] or '' @partial_sequence.setter def partial_sequence(self, value): self.settings.vi['partial_sequence'] = value @property def mode(self): """ Current mode. It isn't guaranteed that the underlying view's .sel() will be in a consistent state (for example, that it will at least have one non-empty region in visual mode. """ return self.settings.vi['mode'] or modes.UNKNOWN @mode.setter def mode(self, value): self.settings.vi['mode'] = value @property def action(self): return self.settings.vi['action'] or None @action.setter def action(self, value): self.settings.vi['action'] = value @property def motion(self): return self.settings.vi['motion'] or None @motion.setter def motion(self, value): self.settings.vi['motion'] = value @property def motion_count(self): return self.settings.vi['motion_count'] or '' @motion_count.setter def motion_count(self, value): self.settings.vi['motion_count'] = value @property def action_count(self): return self.settings.vi['action_count'] or '' @action_count.setter def action_count(self, value): self.settings.vi['action_count'] = value @property def user_input(self): return self.settings.vi['user_input'] or '' @user_input.setter def user_input(self, value): self.settings.vi['user_input'] = value @property def repeat_data(self): """ Stores (type, cmd_name_or_key_seq, , mode) so '.' can use them. `type` may be 'vi' or 'native'. `vi`-commands are executed VIA_PANEL `PressKeys`, while `native`-commands are executed via .run_command(). """ return self.settings.vi['repeat_data'] or None @repeat_data.setter def repeat_data(self, value): self.logger.info("setting repeat data {0}".format(value)) self.settings.vi['repeat_data'] = value @property def last_macro(self): """ Stores the last recorded macro. """ return self.settings.window['_vintageous_last_macro'] or None @last_macro.setter def last_macro(self, value): """ Stores the last recorded macro. """ # FIXME: Check that we're storing a valid macro? self.settings.window['_vintageous_last_macro'] = value @property def recording_macro(self): return self.settings.window['_vintageous_recording_macro'] or False @recording_macro.setter def recording_macro(self, value): # FIXME: Check that we're storing a bool? self.settings.window['_vintageous_recording_macro'] = value @property def count(self): """ Calculates the actual count for the current command. """ c = 1 if self.action_count and not self.action_count.isdigit(): raise ValueError('action count must be a digit') if self.motion_count and not self.motion_count.isdigit(): raise ValueError('motion count must be a digit') if self.action_count: c = int(self.action_count) or 1 if self.motion_count: c *= (int(self.motion_count) or 1) if c < 1: raise ValueError('count must be greater than 0') return c @property def xpos(self): """ Stores the current xpos for carets. """ return self.settings.vi['xpos'] or 0 @xpos.setter def xpos(self, value): if not isinstance(value, int): raise ValueError('xpos must be an int') self.settings.vi['xpos'] = value @property def visual_block_direction(self): """ Stores the current visual block direction for the current selection. """ return self.settings.vi['visual_block_direction'] or directions.DOWN @visual_block_direction.setter def visual_block_direction(self, value): if not isinstance(value, int): raise ValueError('visual_block_direction must be an int') self.settings.vi['visual_block_direction'] = value @property def logger(self): # FIXME: potentially very slow? # return get_logger() global _logger return _logger() @property def register(self): """ Stores the current open register, as requested by the user. """ # TODO: Maybe unify with Registers? # TODO: Validate register name? return self.settings.vi['register'] or '"' @register.setter def register(self, value): if len(str(value)) > 1: raise ValueError('register must be an character') self.logger.info('opening register {0}'.format(value)) self.settings.vi['register'] = value self.capture_register = False def pop_parser(self): parsers = self.input_parsers current = parsers.pop() self.input_parsers = parsers return current def enter_normal_mode(self): self.mode = modes.NORMAL def enter_visual_mode(self): self.mode = modes.VISUAL def enter_visual_line_mode(self): self.mode = modes.VISUAL_LINE def enter_insert_mode(self): self.mode = modes.INSERT def enter_replace_mode(self): self.mode = modes.REPLACE def enter_select_mode(self): self.mode = modes.SELECT def enter_visual_block_mode(self): self.mode = modes.VISUAL_BLOCK def reset_sequence(self): self.sequence = '' def display_status(self): msg = "{0} {1}" mode_name = modes.to_friendly_name(self.mode) mode_name = '-- {0} --'.format(mode_name) if mode_name else '' sublime.status_message(msg.format(mode_name, self.sequence)) def reset_partial_sequence(self): self.partial_sequence = '' def reset_user_input(self): self.input_parsers = [] self.user_input = '' def reset_register_data(self): self.register = '"' self.capture_register = False def must_scroll_into_view(self): return (self.motion and self.motion.get('scroll_into_view')) def scroll_into_view(self): v = sublime.active_window().active_view() # Make sure we show the first caret on the screen, but don't show # its surroundings. v.show(v.sel()[0], False) def reset_command_data(self): # Resets all temporary data needed to build a command or partial # command to their default values. self.update_xpos() if self.must_scroll_into_view(): self.scroll_into_view() self.action = None self.motion = None self.action_count = '' self.motion_count = '' self.reset_sequence() self.reset_partial_sequence() self.reset_user_input() self.reset_register_data() def update_xpos(self): if self.motion and self.motion.get('updates_xpos'): try: xpos = self.view.rowcol(self.view.sel()[0].b)[1] except Exception as e: print(e) raise ValueError('could not set xpos') self.xpos = xpos def reset(self): # TODO: Remove this when we've ported all commands. This is here for # retrocompatibility. self.reset_command_data() def reset_volatile_data(self): """ Resets window- or application-wide data to their default values when starting a new Vintageous session. """ self.glue_until_normal_mode = False self.view.run_command('unmark_undo_groups_for_gluing') self.gluing_sequence = False self.non_interactive = False self.reset_during_init = True def _set_parsers(self, command): """ Returns `True` if we've had to run an immediate parser via an input panel. """ if command.get('input'): # XXX: Special case. We should probably find a better solution. if self.recording_macro and (command['input'] == 'vi_q'): # We discard the parser, as we want to be able to press # 'q' to stop the macro recorder. return # Our command requests input from the user. Let's see how we # should go about it. parser_name = command['input'] parser_list = self.input_parsers parser_list.append(parser_name) self.input_parsers = parser_list return self._run_parser_via_panel() def _run_parser_via_panel(self): """ Returns `True` if the current parser needs to be run via a panel. If needed, it runs the input-panel-based parser. """ if not self.input_parsers: return False parser_def = inputs.get(self, self.input_parsers[-1]) if parser_def.type == input_types.VIA_PANEL: # Let the input-collection command collect input. sublime.active_window().run_command(parser_def.command) return True return False def process_user_input(self, key): """ Returns `True` if the current input parser is satistied by @key. """ if not self.input_parsers: return _logger().info('[State] active input parsers: {0}'.format( self.input_parsers)) parser_def = inputs.get(self, self.input_parsers[-1]) # TODO: use translate_key? # XXX: Why can't we use the same logic as below? if key.lower() == '<cr>': _logger().info('[State] <cr> pressed, removing 1 parser') self.pop_parser() if parser_def.on_done: _logger().info('[State] running post parser: {0}'.format( parser_def.on_done)) self.view.window().run_command(parser_def.on_done, {'key': key}) return True self.user_input += key if (self.input_parsers and callable(parser_def.command) and parser_def.command(key)): _logger().info('[State] parser satisfied, removing one parser') self.pop_parser() if parser_def.on_done: _logger().info('[State] running post parser: {0}'.format( parser_def.on_done)) self.view.window().run_command(parser_def.on_done, {'key': key}) return True else: _logger().info('[State] more input expected by parser') # we need to keep collecting input return True def set_command(self, command): """ Sets the current command to @command. @command A command definition as found in `keys.py`. """ if command['type'] == cmd_types.MOTION: if self.runnable(): # We already have a motion, so this looks like an error. raise ValueError('too many motions') self.motion = command if self.mode == modes.OPERATOR_PENDING: self.mode = modes.NORMAL if self._set_parsers(command): return elif command['type'] == cmd_types.ACTION: if self.runnable(): # We already have an action, so this looks like an error. raise ValueError('too many actions') self.action = command if (self.action['motion_required'] and not self.in_any_visual_mode()): self.mode = modes.OPERATOR_PENDING if self._set_parsers(command): return else: self.logger.info("[State] command: {0}".format(command)) raise ValueError('unexpected command type') def in_any_visual_mode(self): return (self.mode in (modes.VISUAL, modes.VISUAL_LINE, modes.VISUAL_BLOCK)) def can_run_action(self): if (self.action and (not self.action['motion_required'] or self.in_any_visual_mode())): return True def get_visual_repeat_data(self): """ Returns the data needed to repeat a visual mode command in normal mode. """ if self.mode != modes.VISUAL: return s0 = self.view.sel()[0] lines = self.view.rowcol(s0.end())[0] - self.view.rowcol(s0.begin())[0] if lines > 0: chars = self.view.rowcol(s0.end())[1] else: chars = s0.size() return (lines, chars) def runnable(self): """ Returns `True` if we can run the state data as it is. """ if self.input_parsers: return False if self.action and self.motion: if self.mode != modes.NORMAL: raise ValueError('wrong mode') return True if self.can_run_action(): if self.mode == modes.OPERATOR_PENDING: raise ValueError('wrong mode') return True if self.motion: if self.mode == modes.OPERATOR_PENDING: raise ValueError('wrong mode') return True return False def eval(self): """ Run data as a command if possible. """ if self.runnable(): action_func = motion_func = None action_cmd = motion_cmd = None if self.action: action_func = getattr(actions, self.action['name'], None) if action_func is None: self.logger.info( '[State] action not implemented: {0}'.format( self.action)) self.reset_command_data() return action_cmd = action_func(self) self.logger.info( '[State] action cmd data: {0}'.format(action_cmd)) if self.motion: motion_func = getattr(motions, self.motion['name'], None) if motion_func is None: self.logger.info( '[State] motion not implemented: {0}'.format( self.motion)) self.reset_command_data() return motion_cmd = motion_func(self) self.logger.info( '[State] motion cmd data: {0}'.format(motion_cmd)) if action_func and motion_func: self.logger.info( '[State] full command, switching to internal normal mode') self.mode = modes.INTERNAL_NORMAL # TODO: Make a requirement that motions and actions take a # 'mode' param. if 'mode' in action_cmd['action_args']: action_cmd['action_args']['mode'] = modes.INTERNAL_NORMAL if 'mode' in motion_cmd['motion_args']: motion_cmd['motion_args']['mode'] = modes.INTERNAL_NORMAL args = action_cmd['action_args'] args['count'] = 1 # let the action run the motion within its edit object so that we don't need to # worry about grouping edits to the buffer. args['motion'] = motion_cmd self.logger.info( '[Stage] motion in motion+action: {0}'.format(motion_cmd)) if self.glue_until_normal_mode and not self.gluing_sequence: # We need to tell Sublime Text now that it should group # all the next edits until we enter normal mode again. sublime.active_window().run_command( 'mark_undo_groups_for_gluing') sublime.active_window().run_command(action_cmd['action'], args) if not self.non_interactive: if self.action['repeatable']: self.repeat_data = ('vi', str(self.sequence), self.mode, None) self.reset_command_data() return if motion_func: self.logger.info( '[State] lone motion cmd: {0}'.format(motion_cmd)) sublime.active_window().run_command(motion_cmd['motion'], motion_cmd['motion_args']) if action_func: self.logger.info('[Stage] lone action cmd '.format(action_cmd)) if self.mode == modes.NORMAL: self.logger.info( '[State] switching to internal normal mode') self.mode = modes.INTERNAL_NORMAL if 'mode' in action_cmd['action_args']: action_cmd['action_args'][ 'mode'] = modes.INTERNAL_NORMAL elif self.mode in (modes.VISUAL, modes.VISUAL_LINE): self.view.add_regions('visual_sel', list(self.view.sel())) # Some commands, like 'i' or 'a', open a series of edits that # need to be grouped together unless we are gluing a larger # sequence through PressKeys. For example, aFOOBAR<Esc> should # be grouped atomically, but not inside a sequence like # iXXX<Esc>llaYYY<Esc>, where we want to group the whole # sequence instead. if self.glue_until_normal_mode and not self.gluing_sequence: sublime.active_window().run_command( 'mark_undo_groups_for_gluing') seq = self.sequence visual_repeat_data = self.get_visual_repeat_data() action = self.action sublime.active_window().run_command(action_cmd['action'], action_cmd['action_args']) if not (self.gluing_sequence and self.glue_until_normal_mode): if action['repeatable']: self.repeat_data = ('vi', seq, self.mode, visual_repeat_data) self.logger.info('running command: action: {0} motion: {1}'.format( self.action, self.motion)) if self.mode == modes.INTERNAL_NORMAL: self.enter_normal_mode() self.reset_command_data()
class VintageState(object): """ Stores per-view state using View.Settings() for storage. """ registers = Registers() context = KeyContext() marks = Marks() macros = {} # We maintain a stack of parsers for user input. user_input_parsers = [] # Let's imitate Sublime Text's .command_history() 'null' value. _latest_repeat_command = ('', None, 0) # Stores the latest register name used for macro recording. It's a volatile value that never # gets reset during command execution. _latest_macro_name = None _is_recording = False _cancel_macro = False def __init__(self, view): self.view = view # We have two types of settings: vi-specific (settings.vi) and regular ST view settings # (settings.view). self.settings = SettingsManager(self.view) def enter_normal_mode(self): self.settings.view['command_mode'] = True self.settings.view['inverse_caret_state'] = True # Xpos must be updated every time we return to normal mode, because it doesn't get # updated while in insert mode. self.xpos = None if not self.view.sel() else self.view.rowcol( self.view.sel()[0].b)[1] if self.view.overwrite_status(): self.view.set_overwrite_status(False) # Clear regions outlined by buffer search commands. self.view.erase_regions('vi_search') if not self.buffer_was_changed_in_visual_mode(): # We've been in some visual mode, but we haven't modified the buffer at all. self.view.run_command('unmark_undo_groups_for_gluing') else: # Either we haven't been in any visual mode or we've modified the buffer while in # any visual mode. # # However, there might be cases where we have a clean buffer. For example, we might # have undone our changes, or saved via standard commands. Assume Sublime Text knows # better than us. # # NOTE: There's an issue in S3 where 'glue_marked_undo_groups' will mark the buffer as # dirty even if there are no intervening changes between the 'mark_groups_for_gluing' # and 'glue_marked_undo_groups' calls. That's why we need to explicitly unmark groups # here if the view reports back as clean. if not self.view.is_dirty(): self.view.run_command('unmark_undo_groups_for_gluing') else: self.view.run_command('glue_marked_undo_groups') self.mode = MODE_NORMAL def enter_visual_line_mode(self): self.mode = MODE_VISUAL_LINE def enter_select_mode(self): self.mode = MODE_SELECT def enter_insert_mode(self): self.settings.view['command_mode'] = False self.settings.view['inverse_caret_state'] = False self.mode = MODE_INSERT def enter_visual_mode(self): self.mode = MODE_VISUAL def enter_visual_block_mode(self): self.mode = MODE_VISUAL_BLOCK def enter_normal_insert_mode(self): # This is the mode we enter when we give i a count, as in 5ifoobar<CR><ESC>. self.mode = MODE_NORMAL_INSERT self.settings.view['command_mode'] = False self.settings.view['inverse_caret_state'] = False def enter_replace_mode(self): self.mode = MODE_REPLACE self.settings.view['command_mode'] = False self.settings.view['inverse_caret_state'] = False self.view.set_overwrite_status(True) def store_visual_selections(self): self.view.add_regions('vi_visual_selections', list(self.view.sel())) def buffer_was_changed_in_visual_mode(self): """Returns `True` if we've changed the buffer while in visual mode. """ # XXX: What if we used view.is_dirty() instead? That should be simpler? # XXX: If we can be sure that every modifying command will leave the buffer in a dirty # state, we could go for this solution. # 'maybe_mark_undo_groups_for_gluing' and 'glue_marked_undo_groups' seem to add an entry # to the undo stack regardless of whether intervening modifying-commands have been # issued. # # Example: # 1) We enter visual mode by pressing 'v'. # 2) We exit visual mode by pressing 'v' again. # # Since before the first 'v' and after the second we've called the aforementioned commands, # respectively, we'd now have a new (useless) entry in the undo stack, and the redo stack # would be empty. This would be undesirable, so we need to find out whether marked groups # in visual mode actually need to be glued or not and act based on that information. # FIXME: Design issue. This method won't work always. We have actions like yy that # will make this method return true, while it should return False (since yy isn't a # modifying command). However, yy signals in its own way that it's a non-modifying command. # I don't think this redundancy will cause any bug, but we need to unify nevetheless. if self.mode == MODE_VISUAL: visual_cmd = 'vi_enter_visual_mode' elif self.mode == MODE_VISUAL_LINE: visual_cmd = 'vi_enter_visual_line_mode' else: return True cmds = [] # Set an upper limit to look-ups in the undo stack. for i in range(0, -249, -1): cmd_name, args, _ = self.view.command_history(i) if (cmd_name == 'vi_run' and args['action'] and args['action']['command'] == visual_cmd): break # Sublime Text returns ('', None, 0) when we hit the undo stack's bottom. if not cmd_name: break cmds.append((cmd_name, args)) # If we have an action between v..v calls (or visual line), we have modified the buffer # (most of the time, anyway, there are exceptions that we're not covering here). # TODO: Cover exceptions too, like yy (non-modifying command, though has the shape of a # modifying command). was_modifed = [ name for (name, data) in cmds if data and data.get('action') ] return bool(was_modifed) @property def mode(self): """The current mode. """ return self.settings.vi['mode'] @mode.setter def mode(self, value): self.settings.vi['mode'] = value @property def cancel_macro(self): """Signals whether a running macro should be cancel if, for instance, a motion failed. """ return VintageState._cancel_macro # Should only be called from _vi_run_macro. @cancel_macro.setter def cancel_macro(self, value): VintageState._cancel_macro = value @property def cancel_action(self): """Returns `True` if the current action must be cancelled. """ # If we can't find a suitable action, we should cancel. return self.settings.vi['cancel_action'] @cancel_action.setter def cancel_action(self, value): self.settings.vi['cancel_action'] = value # TODO: Test me. @property def stashed_action(self): return self.settings.vi['stashed_action'] # TODO: Test me. @stashed_action.setter def stashed_action(self, name): self.settings.vi['stashed_action'] = name @property def action(self): """Command's action; must be the name of a function in the `actions` module. """ return self.settings.vi['action'] # TODO: Test me. @action.setter def action(self, action): stored_action = self.settings.vi['action'] target = 'action' # Sometimes we'll receive an incomplete command that may be an action or a motion, like g # or dg, both leading up to gg and dgg, respectively. When there's already a complete # action, though, we already know it must be a motion (as in dg and dgg). # Similarly, if there is an action and a motion, the motion must handle the new name just # received. This would be the case when we have dg and we receive another 'g' (or # anything else, for that matter). if (self.action and INCOMPLETE_ACTIONS.get(action) == ACTION_OR_MOTION or self.motion): # The .motion should handle this. self.motion = action return # Check for digraphs like cc, dd, yy. final_action = action if stored_action and action: final_action, type_ = digraphs.get((stored_action, action), ('', None)) # We didn't find a built-in action; let's try with the plugins. if final_action == '': final_action, type_ = plugin_manager.composite_commands.get( (stored_action, action), ('', None)) # Check for multigraphs like g~g~, g~~. # This sequence would get us here: # * vi_g_action # * vi_tilde # * vi_g_action => vi_g_tilde, STASH # * vi_tilde => vi_g_tilde_vi_g_tilde, DIGRAPH_ACTION if self.stashed_action: final_action, type_ = digraphs.get( (self.stashed_action, final_action), ('', None)) # Some motion digraphs are captured as actions, but need to be stored as motions # instead so that the vi command is evaluated correctly. # Ex: gg (vi_g_action, vi_gg) if type_ == DIGRAPH_MOTION: target = 'motion' # TODO: Encapsulate this in a method. input_type, input_parser = INPUT_FOR_MOTIONS.get( final_action, (None, None)) if input_parser: self.user_input_parsers.append(input_parser) if input_type == INPUT_IMMEDIATE: self.expecting_user_input = True self.settings.vi['action'] = None # We are still in an intermediary step, so do some bookkeeping... elif type_ == STASH: # In this case we need to overwrite the current action differently. self.stashed_action = final_action self.settings.vi['action'] = action self.display_partial_command() return # Avoid recursion. The .reset() method will try to set this property to None, not ''. if final_action == '': # The chord is invalid, so notify that we need to cancel the command in .eval(). self.cancel_action = True return if target == 'action': input_type, input_parser = INPUT_FOR_ACTIONS.get( final_action, (None, None)) if input_parser: self.user_input_parsers.append(input_parser) if input_type == INPUT_IMMEDIATE: self.expecting_user_input = True self.settings.vi[target] = final_action self.display_partial_command() @property def motion(self): """Command's motion; must be the name of a function in the `motions` module. """ return self.settings.vi['motion'] # TODO: Test me. @motion.setter def motion(self, name): if self.action in INCOMPLETE_ACTIONS: # The .action should handle this. self.action = name return # HACK: Translate vi_enter to \n if we're expecting user input. # This enables r\n, for instance. # XXX: I don't understand why the enter key is captured as a motion in this case, though; # the catch-all key binding for user input should have intercepted it. if self.expecting_user_input and name == 'vi_enter': self.view.run_command('collect_user_input', {'character': '\n'}) return # Check for digraphs like gg in dgg. stored_motion = self.motion if stored_motion and name: name, type_ = digraphs.get((stored_motion, name), (None, None)) if type_ != DIGRAPH_MOTION: # We know there's an action because we only check for digraphs when there is one. self.cancel_action = True return motion_name = MOTION_TRANSLATION_TABLE.get((self.action, name), name) input_type, input_parser = INPUT_FOR_MOTIONS.get( motion_name, (None, None)) if input_type == INPUT_IMMEDIATE: self.expecting_user_input = True self.user_input_parsers.append(input_parser) if not input_type and self.user_input_parsers: self.expecting_user_input = True self.settings.vi['motion'] = motion_name self.display_partial_command() @property def motion_digits(self): """Count for the motion, like in 3k. """ return self.settings.vi['motion_digits'] or [] @motion_digits.setter def motion_digits(self, value): self.settings.vi['motion_digits'] = value self.display_partial_command() def push_motion_digit(self, value): digits = self.settings.vi['motion_digits'] if not digits: self.settings.vi['motion_digits'] = [value] self.display_partial_command() return digits.append(value) self.settings.vi['motion_digits'] = digits self.display_partial_command() @property def action_digits(self): """Count for the action, as in 3dd. """ return self.settings.vi['action_digits'] or [] @action_digits.setter def action_digits(self, value): self.settings.vi['action_digits'] = value self.display_partial_command() def push_action_digit(self, value): digits = self.settings.vi['action_digits'] if not digits: self.settings.vi['action_digits'] = [value] self.display_partial_command() return digits.append(value) self.settings.vi['action_digits'] = digits self.display_partial_command() @property def count(self): """Computes and returns the final count, defaulting to 1 if the user didn't provide one. """ motion_count = self.motion_digits and int(''.join( self.motion_digits)) or 1 action_count = self.action_digits and int(''.join( self.action_digits)) or 1 return (motion_count * action_count) @property def user_provided_count(self): """Returns the actual count provided by the user, which may be `None`. """ if not (self.motion_digits or self.action_digits): return None return self.count @property def expecting_register(self): """Signals that we need more input from the user before evaluating the global data. """ return self.settings.vi['expecting_register'] @expecting_register.setter def expecting_register(self, value): self.settings.vi['expecting_register'] = value @property def register(self): """Name of the register provided by the user, as in "ayy. """ return self.settings.vi['register'] or None @register.setter def register(self, name): # TODO: Check for valid register name. self.settings.vi['register'] = name self.expecting_register = False @property def expecting_user_input(self): """Signals that we need more input from the user before evaluating the global data. """ return self.settings.vi['expecting_user_input'] @expecting_user_input.setter def expecting_user_input(self, value): self.settings.vi['expecting_user_input'] = value @property def user_input(self): """Additional data provided by the user, as 'a' in @a. """ return self.settings.vi['user_input'] or '' @user_input.setter def user_input(self, value): self.settings.vi['user_input'] = value # FIXME: Sometimes we set the following property in other places too. self.validate_user_input() # self.expecting_user_input = False self.display_partial_command() def validate_user_input(self): name = '' if len(self.user_input_parsers) == 2: # We have two parsers: one for the motion, one for the action. # Evaluate first the motion's. name = self.motion validator = self.user_input_parsers[-1] elif self.motion and INPUT_FOR_ACTIONS.get( self.action, (None, None))[0] == INPUT_AFTER_MOTION: assert len(self.user_input_parsers) == 1 name = self.action validator = self.user_input_parsers[-1] elif self.motion and self.action: name = self.motion validator = self.user_input_parsers[-1] elif self.action: name = self.action validator = self.user_input_parsers[-1] elif self.motion: name = self.motion validator = self.user_input_parsers[-1] assert validator, "Validator must exist if expecting user input." if validator(self.user_input): if name == self.motion: self.settings.vi['user_motion_input'] = self.user_input self.settings.vi['user_input'] = None elif name == self.action: self.settings.vi['user_action_input'] = self.user_input self.settings.vi['user_input'] = None self.user_input_parsers.pop() if len(self.user_input_parsers) == 0: self.expecting_user_input = False def clear_user_input_buffers(self): self.settings.vi['user_action_input'] = None self.settings.vi['user_motion_input'] = None self.settings.vi['user_input'] = None @property def last_buffer_search(self): """Returns the latest buffer search string or `None`. Used by the n and N commands. """ return self.settings.vi['last_buffer_search'] or None @last_buffer_search.setter def last_buffer_search(self, value): self.settings.vi['last_buffer_search'] = value @property def last_character_search(self): """Returns the latest character search or `None`. Used by the , and ; commands. """ return self.settings.vi['last_character_search'] or None @property def last_character_search_forward(self): """Returns True, False or `None`. Used by the , and ; commands. """ return self.settings.vi['last_character_search_forward'] or None @last_character_search_forward.setter def last_character_search_forward(self, value): # TODO: Should this piece of data be global instead of local to each buffer? self.settings.vi['last_character_search_forward'] = value @last_character_search.setter def last_character_search(self, value): # TODO: Should this piece of data be global instead of local to each buffer? self.settings.vi['last_character_search'] = value @property def xpos(self): """Maintains the current column for the caret in normal and visual mode. """ xpos = self.settings.vi['xpos'] return xpos if isinstance(xpos, int) else None @xpos.setter def xpos(self, value): self.settings.vi['xpos'] = value @property def next_mode(self): """Mode to transition to after the command has been run. For example, ce needs to change to insert mode after it's run. """ next_mode = self.settings.vi['next_mode'] or MODE_NORMAL return next_mode @next_mode.setter def next_mode(self, value): self.settings.vi['next_mode'] = value @property def next_mode_command(self): """Command to make the transitioning to the next mode. """ next_mode_command = self.settings.vi['next_mode_command'] return next_mode_command @next_mode_command.setter def next_mode_command(self, value): self.settings.vi['next_mode_command'] = value @property def repeat_command(self): """Latest modifying command performed. Accessed via '.'. """ # This property is volatile. It won't be persisted between sessions. return VintageState._latest_repeat_command @repeat_command.setter def repeat_command(self, value): VintageState._latest_repeat_command = value @property def is_recording(self): """Signals that we're recording a macro. """ return VintageState._is_recording @is_recording.setter def is_recording(self, value): VintageState._is_recording = value @property def latest_macro_name(self): """Latest macro recorded. Accessed via @@. """ return VintageState._latest_macro_name @latest_macro_name.setter def latest_macro_name(self, value): VintageState._latest_macro_name = value def parse_motion(self): """Returns a CmdData instance with parsed motion data. """ vi_cmd_data = CmdData(self) # This should happen only at initialization. # XXX: This is effectively zeroing xpos. Shouldn't we move this into new_vi_cmd_data()? # XXX: REFACTOR if vi_cmd_data['xpos'] is None: xpos = 0 if self.view.sel(): xpos = self.view.rowcol(self.view.sel()[0].b) self.xpos = xpos vi_cmd_data['xpos'] = xpos # Actions originating in normal mode are run in a pseudomode that helps to distiguish # between visual mode and this case (both use selections, either implicitly or # explicitly). if self.action and (self.mode == MODE_NORMAL): vi_cmd_data['mode'] = _MODE_INTERNAL_NORMAL motion = self.motion motion_func = None if motion: try: motion_func = getattr(motions, self.motion) except AttributeError: raise AttributeError( "Vintageous: Unknown motion: '{0}'".format(self.motion)) if motion_func: vi_cmd_data = motion_func(vi_cmd_data) return vi_cmd_data def parse_action(self, vi_cmd_data): """Updates and returns the passed-in CmdData instance using parsed data about the action. """ try: action_func = getattr(actions, self.action) except AttributeError: try: # We didn't find the built-in function; let's try our luck with plugins. action_func = plugin_manager.actions[self.action] except KeyError: raise AttributeError( "Vintageous: Unknown action: '{0}'".format(self.action)) except TypeError: raise TypeError( "Vintageous: parse_action requires an action be specified.") if action_func: vi_cmd_data = action_func(vi_cmd_data) # Notify global state to go ahead with the command if there are selections and the action # is ready to be run (which is almost always the case except for some digraphs). # NOTE: By virtue of checking for non-empty selections instead of an explicit mode, # the user can run actions on selections created outside of Vintageous. # This seems to work well. if (self.view.has_non_empty_selection_region() and # XXX: This check is pretty useless, because we abort early in .run() anyway. # Logically, it makes sense, however. not vi_cmd_data['is_digraph_start']): vi_cmd_data['motion_required'] = False return vi_cmd_data def eval_cancel_action(self): """Cancels the whole run of the command. """ # TODO: add a .parse() method that includes boths steps? vi_cmd_data = self.parse_motion() vi_cmd_data = self.parse_action(vi_cmd_data) if vi_cmd_data['must_blink_on_error']: utils.blink() # Modify the data that determines the mode we'll end up in when the command finishes. self.next_mode = vi_cmd_data['_exit_mode'] # Since we are exiting early, ensure we leave the selections as the commands wants them. if vi_cmd_data['_exit_mode_command']: self.view.run_command(vi_cmd_data['_exit_mode_command']) def eval_full_command(self): """Evaluates a command like 3dj, where there is an action as well as a motion. """ vi_cmd_data = self.parse_motion() # Sometimes we'll have an incomplete motion, like in dg leading up to dgg. In this case, # we don't want the vi command evaluated just yet. if vi_cmd_data['is_digraph_start']: return vi_cmd_data = self.parse_action(vi_cmd_data) if not vi_cmd_data['is_digraph_start']: # We are about to run an action, so let Sublime Text know we want all editing # steps folded into a single sequence. "All editing steps" means slightly different # things depending on the mode we are in. if vi_cmd_data['_mark_groups_for_gluing']: self.view.run_command('maybe_mark_undo_groups_for_gluing') self.view.run_command('vi_run', vi_cmd_data) self.reset() else: # If we have a digraph start, the global data is in an invalid state because we # are still missing the complete digraph. Abort and clean up. if vi_cmd_data['_exit_mode'] == MODE_INSERT: # We've been requested to change to this mode. For example, we're looking at # CTRL+r,j in INSERTMODE, which is an invalid sequence. utils.blink() self.reset() self.enter_insert_mode() # We have an invalid command which consists in an action and a motion, like gl. Abort. elif (self.mode == MODE_NORMAL) and self.motion: utils.blink() self.reset() elif self.mode != MODE_NORMAL: # Normally we'd go back to normal mode. self.enter_normal_mode() self.reset() def eval_lone_action(self): """Evaluate lone action like in 'd' or 'esc'. Some actions can be run without a motion. """ vi_cmd_data = self.parse_motion() vi_cmd_data = self.parse_action(vi_cmd_data) if vi_cmd_data['is_digraph_start']: # XXX: When does this happen? Why are we only interested in MODE_NORMAL? # XXX In response to the above, this must be due to Ctrl+r. if vi_cmd_data['_change_mode_to'] == MODE_NORMAL: self.enter_normal_mode() # We know we are not ready. return if not vi_cmd_data['motion_required']: # We are about to run an action, so let Sublime Text know we want all editing # steps folded into a single sequence. "All editing steps" means slightly different # things depending on the mode we are in. if vi_cmd_data['_mark_groups_for_gluing']: self.view.run_command('maybe_mark_undo_groups_for_gluing') self.view.run_command('vi_run', vi_cmd_data) self.reset() # TODO: Test me. def eval(self): """Examines the current state and decides whether to actually run the action/motion. """ if self.cancel_action: self.eval_cancel_action() self.reset() utils.blink() elif self.expecting_user_input: return # Action + motion, like in '3dj'. elif self.action and self.motion: self.eval_full_command() # Motion only, like in '3j'. elif self.motion: vi_cmd_data = self.parse_motion() self.view.run_command('vi_run', vi_cmd_data) self.reset() # Action only, like in 'd' or 'esc'. Some actions can be executed without a motion. elif self.action: self.eval_lone_action() def reset(self): """Reset global state. """ had_action = self.action self.motion = None self.action = None self.stashed_action = None self.register = None self.clear_user_input_buffers() self.user_input_parsers.clear() self.expecting_register = False self.expecting_user_input = False self.cancel_action = False sublime.set_timeout( lambda: self.view.erase_regions('vi_training_wheels'), 300) # In MODE_NORMAL_INSERT, we temporarily exit NORMAL mode, but when we get back to # it, we need to know the repeat digits, so keep them. An example command for this case # would be 5ifoobar\n<esc> starting in NORMAL mode. if self.mode == MODE_NORMAL_INSERT: return self.motion_digits = [] self.action_digits = [] if self.next_mode in (MODE_NORMAL, MODE_INSERT): if self.next_mode_command: self.view.run_command(self.next_mode_command) # Sometimes we'll reach this point after performing motions. If we have a stored repeat # command in view A, we switch to view B and do a motion, we don't want .update_repeat_command() # to inspect view B's undo stack and grab its latest modifying command; we want to keep # view A's instead, which is what's stored in _latest_repeat_command. We only want to # update this when there is a new action. # FIXME: Even that will fail when we perform an action that does not modify the buffer, # like splitting the window. The current view's latest modifying command will overwrite # the genuine _latest_repeat_command. The correct solution seems to be to tag every single # modifying command with a 'must_update_repeat_command' attribute. if had_action: self.update_repeat_command() self.next_mode = MODE_NORMAL self.next_mode_command = None def update_repeat_command(self): """Vintageous manages the repeat command on its own. Vim stores away the latest modifying command as the repeat command, and does not wipe it when undoing. On the contrary, Sublime Text will update the repeat command as soon as you undo past the current one. The then previous latest modifying command becomes the new repeat command, and so on. """ cmd, args, times = self.view.command_history(0, True) if not cmd: return elif cmd == 'vi_run' and args.get('action'): self.repeat_command = cmd, args, times elif cmd == 'sequence': # XXX: We are assuming every 'sequence' command is a modifying command, which seems # to be reasonable, but I dunno. self.repeat_command = cmd, args, times elif cmd != 'vi_run': # XXX: We are assuming every 'native' command is a modifying commmand, but it doesn't # feel right... self.repeat_command = cmd, args, times def update_xpos(self): xpos = 0 try: first_sel = self.view.sel()[0] except IndexError: # XXX: Perhaps it's better to leave the xpos untouched? self.xpos = xpos return if self.mode == MODE_VISUAL: if first_sel.a < first_sel.b: xpos = self.view.rowcol(first_sel.b - 1)[1] elif first_sel.a > first_sel.b: xpos = self.view.rowcol(first_sel.b)[1] elif self.mode == MODE_NORMAL: xpos = self.view.rowcol(first_sel.b)[1] self.xpos = xpos def display_partial_command(self): mode_name = mode_to_str(self.mode) or "" mode_name = "-- %s --" % mode_name if mode_name else "" msg = "{0} {1} {2} {3} {4} {5}" action_count = ''.join(self.action_digits) or '' action = self.stashed_action or self.action or '' motion_count = ''.join(self.motion_digits) or '' motion = self.motion or '' motion_input = self.settings.vi['user_motion_input'] or '' action_input = self.user_input or '' if (action and motion) or motion: msg = msg.format(action_count, action, motion_count, motion, motion_input, action_input) elif action: msg = msg.format(motion_count, action, action_count, motion, motion_input, action_input) else: msg = msg.format(action_count, action, motion_count, motion, motion_input, action_input) sublime.status_message(mode_name + ' ' + msg)