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 }