github.com/aretext/aretext@v1.3.0/state/macro.go (about)

     1  package state
     2  
     3  import "log"
     4  
     5  // MacroAction is a transformation of editor state that can be recorded and replayed.
     6  type MacroAction func(*EditorState)
     7  
     8  // MacroState stores recorded macros.
     9  // The "last action" macro is used to repeat the last logical action
    10  // (using the "." command in normal mode).
    11  type MacroState struct {
    12  	lastActions            []MacroAction
    13  	isRecordingUserMacro   bool
    14  	isReplayingUserMacro   bool
    15  	userMacroActions       []MacroAction
    16  	stagedUserMacroActions []MacroAction
    17  }
    18  
    19  // AddToLastActionMacro adds an action to the "last action" macro.
    20  func AddToLastActionMacro(s *EditorState, action MacroAction) {
    21  	s.macroState.lastActions = append(s.macroState.lastActions, action)
    22  }
    23  
    24  // ClearLastActionMacro resets the "last action" macro.
    25  func ClearLastActionMacro(s *EditorState) {
    26  	s.macroState.lastActions = nil
    27  }
    28  
    29  // ReplayLastActionMacro executes the actions recorded in the "last action" macro.
    30  func ReplayLastActionMacro(s *EditorState, count uint64) {
    31  	if s.macroState.isRecordingUserMacro {
    32  		// Replaying a last action macro while recording a user macro can cause an infinite loop:
    33  		// 1) Run a command, recorded as a last action macro.
    34  		// 2) Start recording a user macro.
    35  		// 3) Replay the last action macro.
    36  		// 4) Stop recording the user macro.
    37  		// 5) Replay the user macro.
    38  		// 6) Replay the last action macro.
    39  		//
    40  		// In step (5), the last action macro becomes the replay of the user macro.
    41  		// When executed, the user macro invokes the last action macro, which invokes
    42  		// the user macro, ad infinitum.
    43  		//
    44  		// Avoid this problem by disallowing replay while recording entirely.
    45  		SetStatusMsg(s, StatusMsg{
    46  			Style: StatusMsgStyleError,
    47  			Text:  "Cannot repeat the last action while recording a macro",
    48  		})
    49  		return
    50  	}
    51  
    52  	for i := uint64(0); i < count; i++ {
    53  		for _, action := range s.macroState.lastActions {
    54  			action(s)
    55  		}
    56  	}
    57  }
    58  
    59  // ToggleUserMacroRecording stops/starts recording a user-defined macro.
    60  // If recording stops before any actions have been recorded, the previously-recorded
    61  // macro will be preserved.
    62  func ToggleUserMacroRecording(s *EditorState) {
    63  	m := &s.macroState
    64  	if m.isRecordingUserMacro {
    65  		log.Printf("Stopped recording user macro\n")
    66  		m.isRecordingUserMacro = false
    67  
    68  		if len(m.stagedUserMacroActions) == 0 {
    69  			// The user probably started recording by mistake and wouldn't
    70  			// want to lose the previously-recorded macro.
    71  			SetStatusMsg(s, StatusMsg{
    72  				Style: StatusMsgStyleSuccess,
    73  				Text:  "Cancelled macro recording",
    74  			})
    75  			return
    76  		}
    77  
    78  		m.userMacroActions = m.stagedUserMacroActions
    79  		m.stagedUserMacroActions = nil
    80  		SetStatusMsg(s, StatusMsg{
    81  			Style: StatusMsgStyleSuccess,
    82  			Text:  "Recorded macro",
    83  		})
    84  	} else {
    85  		log.Printf("Started recording user macro\n")
    86  		m.isRecordingUserMacro = true
    87  		m.stagedUserMacroActions = nil
    88  		SetStatusMsg(s, StatusMsg{
    89  			Style: StatusMsgStyleSuccess,
    90  			Text:  "Started recording macro",
    91  		})
    92  	}
    93  }
    94  
    95  // AddToRecordingUserMacro adds an action to the currently recording user macro, if any.
    96  func AddToRecordingUserMacro(s *EditorState, action MacroAction) {
    97  	m := &s.macroState
    98  	if m.isRecordingUserMacro {
    99  		m.stagedUserMacroActions = append(m.stagedUserMacroActions, action)
   100  	}
   101  }
   102  
   103  // ReplayRecordedUserMacro replays the recorded user-defined macro.
   104  // If no macro has been recorded, this shows an error status msg.
   105  func ReplayRecordedUserMacro(s *EditorState) {
   106  	m := &s.macroState
   107  
   108  	if m.isRecordingUserMacro {
   109  		// Replaying a macro while recording a macro would cause unexpected results.
   110  		// On the initial recording, the replay would refer to the previously-recorded macro,
   111  		// but on subsequent replays it would refer to the newly-recorded macro.
   112  		// Avoid this problem by disallowing replay while recording entirely.
   113  		SetStatusMsg(s, StatusMsg{
   114  			Style: StatusMsgStyleError,
   115  			Text:  "Cannot replay a macro while recording a macro",
   116  		})
   117  		return
   118  	}
   119  
   120  	if len(m.userMacroActions) == 0 {
   121  		SetStatusMsg(s, StatusMsg{
   122  			Style: StatusMsgStyleError,
   123  			Text:  "No macro has been recorded",
   124  		})
   125  		return
   126  	}
   127  
   128  	// Copy the actions into a new slice to ensure later recordings
   129  	// do not change the behavior of the replay action.
   130  	replayActions := make([]MacroAction, len(m.userMacroActions))
   131  	copy(replayActions, m.userMacroActions)
   132  
   133  	// Define a new action that replays the macro.
   134  	// The action sets the isReplayingUserMacro flag to disable undo log checkpointing
   135  	// when switching input modes -- this ensures that the next undo operation reverts
   136  	// the entire macro.
   137  	replay := func(s *EditorState) {
   138  		BeginUndoEntry(s)
   139  		s.macroState.isReplayingUserMacro = true
   140  
   141  		log.Printf("Replaying actions from user macro...\n")
   142  		for _, action := range m.userMacroActions {
   143  			action(s)
   144  		}
   145  		log.Printf("Finished replaying actions from user macro\n")
   146  
   147  		s.macroState.isReplayingUserMacro = false
   148  		CommitUndoEntry(s)
   149  	}
   150  
   151  	// Replay the macro, then set the replay action as the new "last" action macro.
   152  	// This lets the user easily repeat the macro using the "." command in normal mode.
   153  	replay(s)
   154  	m.lastActions = []MacroAction{replay}
   155  
   156  	SetStatusMsg(s, StatusMsg{
   157  		Style: StatusMsgStyleSuccess,
   158  		Text:  "Replayed macro",
   159  	})
   160  }