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  }