github.com/aretext/aretext@v1.3.0/app/editor.go (about)

     1  package app
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"path/filepath"
     7  	"time"
     8  
     9  	"github.com/gdamore/tcell/v2"
    10  
    11  	"github.com/aretext/aretext/config"
    12  	"github.com/aretext/aretext/display"
    13  	"github.com/aretext/aretext/input"
    14  	"github.com/aretext/aretext/locate"
    15  	"github.com/aretext/aretext/state"
    16  )
    17  
    18  // Editor is a terminal-based text editing program.
    19  type Editor struct {
    20  	inputInterpreter  *input.Interpreter
    21  	editorState       *state.EditorState
    22  	screen            tcell.Screen
    23  	palette           *display.Palette
    24  	documentLoadCount int
    25  	termEventChan     chan tcell.Event
    26  }
    27  
    28  // NewEditor instantiates a new editor that uses the provided screen.
    29  func NewEditor(screen tcell.Screen, path string, lineNum uint64, configRuleSet config.RuleSet) *Editor {
    30  	screenWidth, screenHeight := screen.Size()
    31  	editorState := state.NewEditorState(
    32  		uint64(screenWidth),
    33  		uint64(screenHeight),
    34  		configRuleSet,
    35  		suspendScreenFunc(screen),
    36  	)
    37  	inputInterpreter := input.NewInterpreter()
    38  	palette := display.NewPalette()
    39  	documentLoadCount := editorState.DocumentLoadCount()
    40  	termEventChan := make(chan tcell.Event, 1)
    41  	editor := &Editor{
    42  		inputInterpreter,
    43  		editorState,
    44  		screen,
    45  		palette,
    46  		documentLoadCount,
    47  		termEventChan,
    48  	}
    49  
    50  	// Attempt to load the file.
    51  	// If it doesn't exist, this will start with an empty document
    52  	// that the user can edit and save to the specified path.
    53  	state.LoadDocument(
    54  		editorState,
    55  		effectivePath(path),
    56  		false,
    57  		func(p state.LocatorParams) uint64 {
    58  			return locate.StartOfLineNum(p.TextTree, lineNum)
    59  		},
    60  	)
    61  
    62  	return editor
    63  }
    64  
    65  func effectivePath(path string) string {
    66  	if path == "" {
    67  		// If no path is specified, set a default that is probably unique.
    68  		// The user can treat this as a scratchpad or discard it and open another file.
    69  		path = fmt.Sprintf("untitled-%d.txt", time.Now().Unix())
    70  	}
    71  
    72  	absPath, err := filepath.Abs(path)
    73  	if err != nil {
    74  		log.Printf("Error converting %q to absolute path: %v", path, fmt.Errorf("filepath.Abs: %w", err))
    75  		return path
    76  	}
    77  
    78  	return absPath
    79  }
    80  
    81  // RunEventLoop processes events and draws to the screen, blocking until the user exits the program.
    82  func (e *Editor) RunEventLoop() {
    83  	e.redraw(true)
    84  	go e.pollTermEvents()
    85  	e.runMainEventLoop()
    86  	e.shutdown()
    87  }
    88  
    89  func (e *Editor) pollTermEvents() {
    90  	for {
    91  		event := e.screen.PollEvent()
    92  		e.termEventChan <- event
    93  	}
    94  }
    95  
    96  func (e *Editor) runMainEventLoop() {
    97  	var inBracketedPaste bool
    98  	for {
    99  		select {
   100  		case event := <-e.termEventChan:
   101  			e.handleTermEvent(event)
   102  			if pasteEvent, ok := event.(*tcell.EventPaste); ok {
   103  				inBracketedPaste = pasteEvent.Start()
   104  			}
   105  
   106  		case actionFunc := <-e.editorState.TaskResultChan():
   107  			log.Printf("Task completed, executing resulting action...\n")
   108  			actionFunc(e.editorState)
   109  
   110  		case <-e.editorState.FileWatcher().ChangedChan():
   111  			e.handleFileChanged()
   112  		}
   113  
   114  		e.handleIfDocumentLoaded()
   115  
   116  		if e.editorState.QuitFlag() {
   117  			log.Printf("Quit flag set, exiting event loop...\n")
   118  			return
   119  		}
   120  
   121  		// Redraw unless there are pending terminal events to process first
   122  		// or we're in the middle of a bracketed paste.
   123  		// This helps avoid the overhead of redrawing after every keypress
   124  		// if the user pastes a lot of text into the terminal emulator.
   125  		if len(e.termEventChan) == 0 && !inBracketedPaste {
   126  			e.redraw(false)
   127  		}
   128  	}
   129  }
   130  
   131  func (e *Editor) handleTermEvent(event tcell.Event) {
   132  	inputCtx := input.ContextFromEditorState(e.editorState)
   133  	actionFunc := e.inputInterpreter.ProcessEvent(event, inputCtx)
   134  	actionFunc(e.editorState)
   135  }
   136  
   137  func (e *Editor) handleFileChanged() {
   138  	log.Printf("File change detected, reloading file...\n")
   139  	state.AbortIfUnsavedChanges(e.editorState, "", state.ReloadDocument)
   140  }
   141  
   142  func (e *Editor) handleIfDocumentLoaded() {
   143  	documentLoadCount := e.editorState.DocumentLoadCount()
   144  	if documentLoadCount != e.documentLoadCount {
   145  		log.Printf("Detected document loaded, updating editor")
   146  
   147  		// Reset the input interpreter, which may have state from the prev document.
   148  		e.inputInterpreter = input.NewInterpreter()
   149  
   150  		// Update palette, since the configuration might have changed.
   151  		styles := e.editorState.Styles()
   152  		e.palette = display.NewPaletteFromConfigStyles(styles)
   153  
   154  		// Store the new document load count so we know when the next document loads.
   155  		e.documentLoadCount = documentLoadCount
   156  	}
   157  }
   158  
   159  func (e *Editor) shutdown() {
   160  	e.editorState.FileWatcher().Stop()
   161  }
   162  
   163  func (e *Editor) redraw(sync bool) {
   164  	inputMode := e.editorState.InputMode()
   165  	inputBufferString := e.inputInterpreter.InputBufferString(inputMode)
   166  	display.DrawEditor(e.screen, e.palette, e.editorState, inputBufferString)
   167  	if sync {
   168  		e.screen.Sync()
   169  	} else {
   170  		e.screen.Show()
   171  	}
   172  }
   173  
   174  func suspendScreenFunc(screen tcell.Screen) state.SuspendScreenFunc {
   175  	return func(f func() error) error {
   176  		// Suspend input processing and reset the terminal to its original state.
   177  		if err := screen.Suspend(); err != nil {
   178  			return fmt.Errorf("screen.Suspend: %w", err)
   179  		}
   180  
   181  		// Ensure screen is resumed after executing the function.
   182  		defer func() {
   183  			if err := screen.Resume(); err != nil {
   184  				log.Printf("Error resuming screen: %v\n", err)
   185  			}
   186  		}()
   187  
   188  		// Execute the function.
   189  		return f()
   190  	}
   191  }