github.com/aretext/aretext@v1.3.0/input/interpreter.go (about) 1 package input 2 3 import ( 4 "embed" 5 "fmt" 6 "log" 7 "strings" 8 9 "github.com/gdamore/tcell/v2" 10 11 "github.com/aretext/aretext/input/engine" 12 "github.com/aretext/aretext/state" 13 ) 14 15 // Interpreter translates key events to commands. 16 type Interpreter struct { 17 modes map[state.InputMode]*mode 18 inBracketedPaste bool 19 pasteBuffer strings.Builder 20 } 21 22 // NewInterpreter creates a new interpreter. 23 func NewInterpreter() *Interpreter { 24 return &Interpreter{ 25 modes: map[state.InputMode]*mode{ 26 // normal mode is used for navigating text. 27 state.InputModeNormal: { 28 name: "normal", 29 commands: NormalModeCommands(), 30 runtime: runtimeForMode(NormalModePath), 31 }, 32 33 // insert mode is used for inserting characters into the document. 34 state.InputModeInsert: { 35 name: "insert", 36 commands: InsertModeCommands(), 37 runtime: runtimeForMode(InsertModePath), 38 }, 39 40 // visual mode is used to visually select a region of the document. 41 state.InputModeVisual: { 42 name: "visual", 43 commands: VisualModeCommands(), 44 runtime: runtimeForMode(VisualModePath), 45 }, 46 47 // menu mode allows the user to search for and select items in a menu. 48 state.InputModeMenu: { 49 name: "menu", 50 commands: MenuModeCommands(), 51 runtime: runtimeForMode(MenuModePath), 52 }, 53 54 // search mode is used to search the document for a substring. 55 state.InputModeSearch: { 56 name: "search", 57 commands: SearchModeCommands(), 58 runtime: runtimeForMode(SearchModePath), 59 }, 60 61 // task mode is used while a task is running asynchronously. 62 // This allows the user to cancel the task if it takes too long. 63 state.InputModeTask: { 64 name: "task", 65 commands: TaskModeCommands(), 66 runtime: runtimeForMode(TaskModePath), 67 }, 68 69 // textfield mode allows the user to enter a text response to a prompt. 70 state.InputModeTextField: { 71 name: "textfield", 72 commands: TextFieldCommands(), 73 runtime: runtimeForMode(TextFieldModePath), 74 }, 75 }, 76 } 77 } 78 79 // ProcessEvent interprets a terminal input event as an action. 80 // (If there is no action, then EmptyAction will be returned.) 81 func (inp *Interpreter) ProcessEvent(event tcell.Event, ctx Context) Action { 82 switch event := event.(type) { 83 case *tcell.EventKey: 84 if inp.inBracketedPaste { 85 return inp.processPasteKey(event) 86 } 87 return inp.processKeyEvent(event, ctx) 88 case *tcell.EventPaste: 89 if event.Start() { 90 return inp.processPasteStart() 91 } else { 92 return inp.processPasteEnd(ctx) 93 } 94 case *tcell.EventResize: 95 return inp.processResizeEvent(event) 96 default: 97 return EmptyAction 98 } 99 } 100 101 func (inp *Interpreter) processKeyEvent(event *tcell.EventKey, ctx Context) Action { 102 log.Printf("Processing key %s in mode %s\n", event.Name(), ctx.InputMode) 103 mode := inp.modes[ctx.InputMode] 104 return mode.ProcessKeyEvent(event, ctx) 105 } 106 107 func (inp *Interpreter) processPasteStart() Action { 108 inp.inBracketedPaste = true 109 return EmptyAction 110 } 111 112 func (inp *Interpreter) processPasteKey(event *tcell.EventKey) Action { 113 switch event.Key() { 114 // Most terminals send KeyEnter (ASCII code 13 = carriage return) 115 // but alacritty sends KeyLF (ASCII code 10 = line feed). 116 case tcell.KeyEnter, tcell.KeyLF: 117 inp.pasteBuffer.WriteRune('\n') 118 case tcell.KeyTab: 119 inp.pasteBuffer.WriteRune('\t') 120 case tcell.KeyRune: 121 inp.pasteBuffer.WriteRune(event.Rune()) 122 } 123 return EmptyAction 124 } 125 126 func (inp *Interpreter) processPasteEnd(ctx Context) Action { 127 text := inp.pasteBuffer.String() 128 inp.inBracketedPaste = false 129 inp.pasteBuffer.Reset() 130 131 switch ctx.InputMode { 132 case state.InputModeInsert: 133 return InsertFromBracketedPaste(text) 134 case state.InputModeNormal, state.InputModeVisual: 135 return ShowStatusMsgBracketedPasteWrongMode 136 case state.InputModeMenu: 137 return BracketedPasteIntoMenuSearch(text) 138 case state.InputModeSearch: 139 return BracketedPasteIntoSearchQuery(text) 140 default: 141 return EmptyAction 142 } 143 } 144 145 func (inp *Interpreter) processResizeEvent(event *tcell.EventResize) Action { 146 log.Printf("Processing resize event\n") 147 width, height := event.Size() 148 return func(s *state.EditorState) { 149 state.ResizeView(s, uint64(width), uint64(height)) 150 state.ScrollViewToCursor(s) 151 } 152 } 153 154 // InputBufferString returns a string describing buffered input events. 155 // It can be displayed to the user to help them understand the input state. 156 func (inp *Interpreter) InputBufferString(mode state.InputMode) string { 157 return inp.modes[mode].InputBufferString() 158 } 159 160 const ( 161 NormalModePath = "generated/normal.bin" 162 InsertModePath = "generated/insert.bin" 163 VisualModePath = "generated/visual.bin" 164 MenuModePath = "generated/menu.bin" 165 SearchModePath = "generated/search.bin" 166 TaskModePath = "generated/task.bin" 167 TextFieldModePath = "generated/textfield.bin" 168 ) 169 170 //go:generate go run generate.go 171 //go:embed generated/* 172 var generatedFiles embed.FS 173 174 // runtimeForMode loads a state machine for an input mode. 175 // The state machine is serialized and embedded in the aretext binary. 176 // See input/generate.go for the code that compiles the state machines. 177 func runtimeForMode(path string) *engine.Runtime { 178 // This should be long enough for any valid input sequence, 179 // but not long enough that count params can overflow uint64. 180 const maxInputLen = 64 181 182 data, err := generatedFiles.ReadFile(path) 183 if err != nil { 184 log.Fatalf("Could not read %s: %s", path, err) 185 } 186 187 stateMachine, err := engine.Deserialize(data) 188 if err != nil { 189 log.Fatalf("Could not deserialize state machine %s: %s", path, err) 190 } 191 192 return engine.NewRuntime(stateMachine, maxInputLen) 193 } 194 195 // mode is an editor input mode. 196 // Each mode has its own rules for interpreting user input. 197 type mode struct { 198 name string 199 commands []Command 200 runtime *engine.Runtime 201 inputBuffer strings.Builder 202 } 203 204 func (m *mode) ProcessKeyEvent(event *tcell.EventKey, ctx Context) Action { 205 engineEvent := eventKeyToEngineEvent(event) 206 if event.Key() == tcell.KeyRune { 207 m.inputBuffer.WriteRune(event.Rune()) 208 } 209 210 action := EmptyAction 211 result := m.runtime.ProcessEvent(engineEvent) 212 if result.Decision == engine.DecisionAccept { 213 command := m.commands[result.CmdId] 214 params := capturesToCommandParams(result.Captures) 215 log.Printf( 216 "%s mode accepted input for command %q with params %+v and ctx %+v\n", 217 m.name, command.Name, 218 params, ctx, 219 ) 220 221 if err := m.validateParams(command, params); err != nil { 222 action = func(s *state.EditorState) { 223 state.SetStatusMsg(s, state.StatusMsg{ 224 Style: state.StatusMsgStyleError, 225 Text: err.Error(), 226 }) 227 } 228 } else { 229 action = command.BuildAction(ctx, params) 230 } 231 } 232 233 if result.Decision != engine.DecisionWait { 234 m.inputBuffer.Reset() 235 } 236 237 return action 238 } 239 240 func (m *mode) validateParams(command Command, params CommandParams) error { 241 if command.MaxCount > 0 && params.Count > command.MaxCount { 242 return fmt.Errorf("count must be less than or equal to %d", command.MaxCount) 243 } 244 return nil 245 } 246 247 func (m *mode) InputBufferString() string { 248 return m.inputBuffer.String() 249 }