/****************************************************************************
 * apps/system/vi/vi.c
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <sys/stat.h>

#include <stdarg.h>
#include <stdint.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdarg.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <errno.h>
#include <debug.h>

#include <system/termcurses.h>
#include <graphics/curses.h>

#include <nuttx/ascii.h>
#include <nuttx/vt100.h>

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#ifndef CONFIG_SYSTEM_VI_ROWS
#  define CONFIG_SYSTEM_VI_ROWS 16
#endif

#ifndef CONFIG_SYSTEM_VI_COLS
#  define CONFIG_SYSTEM_VI_COLS 64
#endif

/* Some environments may return CR as end-of-line, others LF, and others
 * both.  If not specified, the logic here assumes either (but not both) as
 * the default.
 */

#if defined(CONFIG_EOL_IS_CR)
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#  undef  CONFIG_EOL_IS_EITHER_CRLF
#elif defined(CONFIG_EOL_IS_LF)
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#  undef  CONFIG_EOL_IS_EITHER_CRLF
#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_EITHER_CRLF
#elif defined(CONFIG_EOL_IS_EITHER_CRLF)
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#else
#  undef  CONFIG_EOL_IS_CR
#  undef  CONFIG_EOL_IS_LF
#  undef  CONFIG_EOL_IS_BOTH_CRLF
#  define CONFIG_EOL_IS_EITHER_CRLF 1
#endif

#ifndef CONFIG_SYSTEM_VI_YANK_THRESHOLD
#define CONFIG_SYSTEM_VI_YANK_THRESHOLD 128
#endif

/* Control characters */

#undef  CTRL
#define CTRL(a)         ((a) & 0x1f)

#define VI_BEL(vi)      vi_putch(vi,CTRL('G'))

/* Sizes of things */

#define MAX_STRING      64   /* The maximum size of a filename or search string */
#define MAX_FILENAME    128  /* The maximum size of a filename or search string */
#define SCRATCH_BUFSIZE 128  /* The maximum size of the scratch buffer */
#define CMD_BUFSIZE     128  /* The maximum size of the scratch buffer */

#define TEXT_GULP_SIZE  512  /* Text buffer allocations are managed with this unit */
#define TEXT_GULP_MASK  511  /* Mask for aligning buffer allocation sizes */
#define ALIGN_GULP(x)   (((x) + TEXT_GULP_MASK) & ~TEXT_GULP_MASK)

#define VI_TABSIZE      8    /* A TAB is eight characters */
#define TABMASK         7    /* Mask for TAB alignment */
#define NEXT_TAB(p)     (((p) + VI_TABSIZE) & ~TABMASK)

/* Parsed command action bits */

#define CMD_READ        (1 << 0) /* Bit 0: Read */
#define CMD_WRITE_MASK  (3 << 1) /* Bits 1-2: x1=Write operation */
#  define CMD_WRITE     (1 << 1) /*           01=Write (without overwriting) */
#  define CMD_OWRITE    (3 << 1) /*           11=Overwrite */
#define CMD_QUIT_MASK   (3 << 3) /* Bits 3-4: x1=Quit operation */
#  define CMD_QUIT      (1 << 3) /*           01=Quit if saved */
#  define CMD_DISCARD   (3 << 3) /*           11=Quit without saving */

#define CMD_NONE        (0)                     /* No command */
#define CMD_WRITE_QUIT  (CMD_WRITE | CMD_QUIT)  /* Write file and quit command */
#define CMD_OWRITE_QUIT (CMD_OWRITE | CMD_QUIT) /* Overwrite file and quit command */

#define IS_READ(a)      (((uint8_t)(a) & CMD_READ) != 0)
#define IS_WRITE(a)     (((uint8_t)(a) & CMD_WRITE) != 0)
#define IS_OWRITE(a)    (((uint8_t)(a) & CMD_WRITE_MASK) == CMD_OWRITE)
#define IS_NOWRITE(a)   (((uint8_t)(a) & CMD_WRITE_MASK) == CMD_WRITE)
#define IS_QUIT(a)      (((uint8_t)(a) & CMD_QUIT) != 0)
#define IS_DISCARD(a)   (((uint8_t)(a) & CMD_QUIT_MASK) == CMD_DISCARD)
#define IS_NDISCARD(a)  (((uint8_t)(a) & CMD_QUIT_MASK) == CMD_QUIT && !IS_WRITE(a))

#define CMD_FILE_MASK   (CMD_READ | CMD_WRITE)
#define USES_FILE(a)    (((uint8_t)(a) & CMD_FILE_MASK) != 0)

/* Search control */

#define VI_CHAR_SPACE   0
#define VI_CHAR_ALPHA   1
#define VI_CHAR_PUNCT   2
#define VI_CHAR_CRLF    3

/* Output */

#define vi_error(vi, fmt, ...)   vi_printf(vi, "ERROR: ", fmt, ##__VA_ARGS__)
#define vi_message(vi, fmt, ...) vi_printf(vi, NULL, fmt, ##__VA_ARGS__)

/* Debug */

#ifndef CONFIG_SYSTEM_VI_DEBUGLEVEL
#  define CONFIG_SYSTEM_VI_DEBUGLEVEL 0
#endif

#if CONFIG_SYSTEM_VI_DEBUGLEVEL > 0
#  define vidbg(format, ...) \
     syslog(LOG_DEBUG, EXTRA_FMT format EXTRA_ARG, ##__VA_ARGS__)
#  define vvidbg(format, ap) \
     vsyslog(LOG_DEBUG, format, ap)
#else
#  define vidbg(x...)
#  define vvidbg(x...)
#endif

#if CONFIG_SYSTEM_VI_DEBUGLEVEL > 1
#  define viinfo(format, ...) \
     syslog(LOG_DEBUG, EXTRA_FMT format EXTRA_ARG, ##__VA_ARGS__)
#else
#  define viinfo(x...)
#endif

/* Uncomment to enable bottom line debug printing.  Useful during yank /
 * paste debugging, etc.
 */

/* #define ENABLE_BOTTOM_LINE_DEBUG */

/****************************************************************************
 * Private Types
 ****************************************************************************/

/* VI Key Bindings */

enum vi_cmdmode_key_e
{
  KEY_CMDMODE_BEGINLINE   = '0',  /* Move cursor to start of current line */
  KEY_CMDMODE_APPEND      = 'a',  /* Enter insertion mode after current character */
  KEY_CMDMODE_WORDBACK    = 'b',  /* Scan to previous word */
  KEY_CMDMODE_CHANGE      = 'c',  /* Delete text and enter insert mode */
  KEY_CMDMODE_DEL_LINE    = 'd',  /* "dd" deletes a lines */
  KEY_CMDMODE_FINDINLINE  = 'f',  /* Find within current line */
  KEY_CMDMODE_GOTOTOP     = 'g',  /* Two of these sends cursor to the top */
  KEY_CMDMODE_LEFT        = 'h',  /* Move left one character */
  KEY_CMDMODE_INSERT      = 'i',  /* Enter insertion mode before current character */
  KEY_CMDMODE_DOWN        = 'j',  /* Move down one line */
  KEY_CMDMODE_UP          = 'k',  /* Move up one line */
  KEY_CMDMODE_RIGHT       = 'l',  /* Move right one character */
  KEY_CMDMODE_MARK        = 'm',  /* Place a mark beginning at the current cursor position */
  KEY_CMDMODE_FINDNEXT    = 'n',  /* Find next */
  KEY_CMDMODE_OPENBELOW   = 'o',  /* Enter insertion mode in new line below current */
  KEY_CMDMODE_PASTE       = 'p',  /* Paste line(s) from into text after current line */
  KEY_CMDMODE_REPLACECH   = 'r',  /* Replace character(s) under cursor */
  KEY_CMDMODE_SUBSTITUTE  = 's',  /* Substitute character with new text */
  KEY_CMDMODE_TFINDINLINE = 't',  /* Find within current line, cursor at previous char */
  KEY_CMDMODE_WORDFWD     = 'w',  /* Scan to next word */
  KEY_CMDMODE_DEL         = 'x',  /* Delete a single character */
  KEY_CMDMODE_YANK        = 'y',  /* "yy" yanks the current line(s) into the buffer */

  KEY_CMDMODE_CR          = '\r', /* CR moves to first non-space on next line */
  KEY_CMDMODE_NL          = '\n', /* NL moves to first non-space on next line */

  KEY_CMDMODE_APPENDEND   = 'A',  /* Enter insertion mode at the end of the current line */
  KEY_CMDMODE_CHANGETOEOL = 'C',  /* Change (del to EOL and go to insert mode */
  KEY_CMDMODE_DELTOEOL    = 'D',  /* Delete to End Of Line */
  KEY_CMDMODE_GOTO        = 'G',  /* Got to line */
  KEY_CMDMODE_TOP         = 'H',  /* Got to top of screen */
  KEY_CMDMODE_JOIN        = 'J',  /* Join line below with current line */
  KEY_CMDMODE_BOTTOM      = 'L',  /* Got to bottom of screen */
  KEY_CMDMODE_INSBEGIN    = 'I',  /* Enter insertion mode at the beginning of the current */
  KEY_CMDMODE_MIDDLE      = 'M',  /* Got to middle of screen */
  KEY_CMDMODE_FINDPREV    = 'N',  /* Find previous */
  KEY_CMDMODE_OPENABOVE   = 'O',  /* Enter insertion mode in new line above current line */
  KEY_CMDMODE_PASTEBEFORE = 'P',  /* Paste text before cursor location */
  KEY_CMDMODE_REPLACE     = 'R',  /* Replace character(s) under cursor until ESC */
  KEY_CMDMODE_DELBACKWARD = 'X',  /* Replace character(s) under cursor until ESC */
  KEY_CMDMODE_SAVEQUIT    = 'Z',  /* Another one is the same as "wq" */

  KEY_CMDMODE_COLONMODE   = ':',  /* The next character command prefaced with a colon */
  KEY_CMDMODE_FINDMODE    = '/',  /* Enter forward search string */
  KEY_CMDMODE_ENDLINE     = '$',  /* Move cursor to end of current line */
  KEY_CMDMODE_REVFINDMODE = '?',  /* Enter forward search string */
  KEY_CMDMODE_REPEAT      = '.',  /* Repeat last command */
  KEY_CMDMODE_FIRSTCHAR   = '^',  /* Find first non-whitespace character on line */
  KEY_CMDMODE_NEXTLINE    = '+',  /* Find first non-whitespace character on line */
  KEY_CMDMODE_PREVLINE    = '-',  /* Find first non-whitespace character on line */

  KEY_CMDMODE_PAGEUP      = CTRL('b'), /* Move backward one screen */
  KEY_CMDMODE_HALFDOWN    = CTRL('d'), /* Move down (forward) one half screen */
  KEY_CMDMODE_PAGEDOWN    = CTRL('f'), /* Move forward one screen */
  KEY_CMDMODE_REDRAW      = CTRL('l'), /* Redraws the screen */
  KEY_CMDMODE_REDRAW2     = CTRL('r'), /* Redraws the screen, removing deleted lines */
  KEY_CMDMODE_HALFUP      = CTRL('u')  /* Move up (back) one half screen */
};

enum vi_insmode_key_e
{
  KEY_INSMODE_QUOTE       = '\\', /* The next character is quote (use literal value) */
};

enum vi_colonmode_key_e
{
  KEY_COLMODE_READ        = 'r',  /* Read file */
  KEY_COLMODE_QUIT        = 'q',  /* Quit vi */
  KEY_COLMODE_WRITE       = 'w',  /* Write file */
  KEY_COLMODE_FORCE       = '!',  /* Force operation */
  KEY_COLMODE_QUOTE       = '\\'  /* The next character is quote (use literal value) */
};

enum vi_findmode_key_e
{
  KEY_FINDMODE_QUOTE      = '\\'  /* The next character is quote (use literal value) */
};

/* VI modes */

enum vi_mode_s
{
  MODE_COMMAND            = 0,    /* ESC         Command mode */
  SUBMODE_COLON,                  /* :           Command sub-mode */
  SUBMODE_FIND,                   /* /           Search sub-mode */
  SUBMODE_REVFIND,                /* ?           Search sub-mode */
  SUBMODE_REPLACECH,              /* r           Replace sub-mode 1 */
  MODE_INSERT,                    /* i,I,a,A,o,O Insert mode */
  MODE_REPLACE,                   /* R           Replace sub-mode 2 */
  MODE_FINDINLINE                 /* f           Find in line sub-mode */
};

/* This structure represents a cursor position */

struct vi_pos_s
{
  uint16_t row;
  uint16_t column;
};

#ifdef CONFIG_SYSTEM_VI_INCLUDE_UNDO
/* The undo structure.  Will be implemented soon. */

struct vi_undo_s
{
  off_t     curpos;                 /* Cursor position before operation */
  off_t     ybytes;                 /* Bytes yanked */
  off_t     ibytes;                 /* Bytes inserted */
  FAR char *yank;                   /* The yanked bytes */
  FAR char *insert;                 /* The inserted bytes */
  uint8_t   type;                   /* Type of operation */
  uint8_t   complete;               /* Indicates if this operation complete */
};
#endif

/* This structure describes the overall state of the editor */

struct vi_s
{
  struct vi_pos_s cursor;   /* Current cursor position */
  struct vi_pos_s cursave;  /* Saved cursor position */
  struct vi_pos_s display;  /* Display size */
  FAR struct termcurses_s * tcurs;
  off_t curpos;             /* The current cursor offset into the text buffer */
  off_t textsize;           /* The size of the text buffer */
  off_t winpos;             /* Offset corresponding to the start of the display */
  off_t prevpos;            /* Previous display position */
  off_t vscroll;            /* Vertical display offset in rows */
  uint16_t hscroll;         /* Horizontal display offset */
  uint16_t value;           /* Numeric value entered prior to a command */
  uint16_t reqcolumn;       /* Requested column when moving up/down */
  uint8_t mode;             /* See enum vi_mode_s */
  uint8_t cmdlen;           /* Length of the command in the scratch[] buffer */
  bool modified;            /* True: The file has modified */
  bool error;               /* True: There is an error message on the last line */
  bool delarm;              /* Delete text arm flag */
  bool chgarm;              /* Change text arm flag */
  bool yankarm;             /* Yank text arm flag */
  bool toparm;              /* One more 'g' and the cursor moves to the top */
  bool wqarm;               /* One more 'Z' is the same as :wq */
  bool fullredraw;          /* True to draw all lines on screen */
  bool drawtoeos;           /* True to draw all lines to end of screen */
  bool redrawline;          /* True to draw current line */
  bool updatereqcol;        /* True to update the requested column */
  bool tfind;               /* Find in line 't' mode */
  bool revfind;             /* In ? reverse find mode */
  bool yankcharmode;        /* Indicates yank buffer is char vs. line mode */

  /* Buffers */

  FAR char *text;           /* Dynamically allocated text buffer */
  size_t txtalloc;          /* Current allocated size of the text buffer */
  FAR char *yank;           /* Dynamically allocated yank buffer */
  size_t yankalloc;         /* Current allocated size of the yank buffer */
  size_t yanksize;          /* Current size of the text in the yank buffer */

  char filename[MAX_FILENAME];    /* Holds the currently selected filename */
  char findstr[MAX_STRING];       /* Holds the current search string */
  char scratch[SCRATCH_BUFSIZE];  /* For general, scratch usage */

#ifdef CONFIG_SYSTEM_VI_INCLUDE_UNDO
  struct vi_undo_s undo[CONFIG_SYSTEM_VI_UNDO_LEVELS];
  uint16_t undocount;       /* Number of valid undo entries */
  uint16_t undoindex;       /* Current index in undo/redo stack */
#endif

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
  char cmdbuf[CMD_BUFSIZE]; /* Last command buffer */
  uint16_t cmdindex;        /* Current index within cmdbuf */
  uint16_t cmdcount;        /* Count of entries in cmdbuf */
  uint16_t repeatvalue;     /* The command repeat value */
  bool cmdrepeat;           /* Command repeat is active */
#endif
};

/****************************************************************************
 * Private Function Prototypes
 ****************************************************************************/

/* Low-level display and data entry functions */

static void     vi_write(FAR struct vi_s *vi, FAR const char *buffer,
                  size_t buflen);
static void     vi_putch(FAR struct vi_s *vi, char ch);
static int      vi_getch(FAR struct vi_s *vi);
#if 0 /* Not used */
static void     vi_blinkon(FAR struct vi_s *vi);
#endif
static void     vi_boldon(FAR struct vi_s *vi);
static void     vi_reverseon(FAR struct vi_s *vi);
static void     vi_attriboff(FAR struct vi_s *vi);
static void     vi_cursoron(FAR struct vi_s *vi);
static void     vi_cursoroff(FAR struct vi_s *vi);
#if 0 /* Not used */
static void     vi_cursorhome(FAR struct vi_s *vi);
#endif
static void     vi_setcursor(FAR struct vi_s *vi, uint16_t row,
                  uint16_t column);
static void     vi_clrtoeol(FAR struct vi_s *vi);
#if 0 /* Not used */
static void     vi_clrscreen(FAR struct vi_s *vi);
#endif

/* Final Line display */

static void     vi_printf(FAR struct vi_s *vi, FAR const char *prefix,
                  FAR const char *fmt, ...);

/* Line positioning */

static off_t    vi_linebegin(FAR struct vi_s *vi, off_t pos);
static off_t    vi_prevline(FAR struct vi_s *vi, off_t pos);
static off_t    vi_lineend(FAR struct vi_s *vi, off_t pos);
static off_t    vi_nextline(FAR struct vi_s *vi, off_t pos);

/* Text buffer management */

static bool     vi_extendtext(FAR struct vi_s *vi, off_t pos,
                  size_t increment);
static void     vi_shrinkpos(FAR struct vi_s *vi, off_t delpos,
                  size_t delsize, FAR off_t *pos);
static void     vi_shrinktext(FAR struct vi_s *vi, off_t pos, size_t size);

/* File access */

static bool     vi_insertfile(FAR struct vi_s *vi, off_t pos,
                  FAR const char *filename);
static bool     vi_savetext(FAR struct vi_s *vi, FAR const char *filename,
                  off_t pos, size_t size);
static bool     vi_checkfile(FAR struct vi_s *vi, FAR const char *filename);

/* Mode management */

static void     vi_setmode(FAR struct vi_s *vi, uint8_t mode, long value);
static void     vi_setsubmode(FAR struct vi_s *vi, uint8_t mode,
                  char prompt, long value);
static void     vi_exitsubmode(FAR struct vi_s *vi, uint8_t mode);

/* Display management */

static void     vi_windowpos(FAR struct vi_s *vi, off_t start, off_t end,
                  uint16_t *pcolumn, off_t *ppos);
static void     vi_scrollcheck(FAR struct vi_s *vi);
static void     vi_showtext(FAR struct vi_s *vi);
static void     vi_showlinecol(FAR struct vi_s *vi);

/* Command mode */

static void     vi_cusorup(FAR struct vi_s *vi, int nlines);
static void     vi_cursordown(FAR struct vi_s *vi, int nlines);
static off_t    vi_cursorleft(FAR struct vi_s *vi, off_t curpos,
                  int ncolumns);
static off_t    vi_cursorright(FAR struct vi_s *vi, off_t curpos,
                  int ncolumns);
static void     vi_delforward(FAR struct vi_s *vi);
static void     vi_delbackward(FAR struct vi_s *vi);
static void     vi_linerange(FAR struct vi_s *vi, off_t *start, off_t *end);
static void     vi_delline(FAR struct vi_s *vi);
static void     vi_deltoeol(FAR struct vi_s *vi);
static void     vi_yanktext(FAR struct vi_s *vi, off_t start, off_t end,
                  bool yankcharmode, bool del_after_yank);
static void     vi_yank(FAR struct vi_s *vi, bool del_after_yank);
static void     vi_paste(FAR struct vi_s *vi, bool paste_before);
static void     vi_gotoline(FAR struct vi_s *vi);
static void     vi_join(FAR struct vi_s *vi);
static void     vi_cmd_mode(FAR struct vi_s *vi);
static int      vi_gotoscreenbottom(FAR struct vi_s *vi, int rows);
static void     vi_gotofirstnonwhite(FAR struct vi_s *vi);

/* Command sub-modes */

static void     vi_cmdch(FAR struct vi_s *vi, char ch);
static void     vi_cmdbackspace(FAR struct vi_s *vi);

static void     vi_parsecolon(FAR struct vi_s *vi);
static void     vi_cmd_submode(FAR struct vi_s *vi);

static bool     vi_findstring(FAR struct vi_s *vi);
static bool     vi_revfindstring(FAR struct vi_s *vi);
static void     vi_parsefind(FAR struct vi_s *vi, bool revfind);
static void     vi_find_submode(FAR struct vi_s *vi, bool revfind);

static void     vi_replacech(FAR struct vi_s *vi, char ch);
static void     vi_replacech_submode(FAR struct vi_s *vi);

static void     vi_findinline_mode(FAR struct vi_s *vi);

/* Insert and replace modes */

static void     vi_insertch(FAR struct vi_s *vi, char ch);
static void     vi_insert_mode(FAR struct vi_s *vi);

/* Command line processing */

static void     vi_release(FAR struct vi_s *vi);
static void     vi_showusage(FAR struct vi_s *vi, FAR const char *progname,
                  int exitcode);

/* Find next / prev word processing */

static int      vi_chartype(char ch);
static off_t    vi_findnextword(FAR struct vi_s *vi);
static void     vi_gotonextword(FAR struct vi_s *vi);
static off_t    vi_findprevword(FAR struct vi_s *vi);
static void     vi_gotoprevword(FAR struct vi_s *vi);

/* Command repeat processing */

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
static void     vi_saverepeat(FAR struct vi_s *vi, uint16_t ch);
static void     vi_appendrepeat(FAR struct vi_s *vi, uint16_t ch);
#endif

/****************************************************************************
 * Private Data
 ****************************************************************************/

/* VT100 escape sequences */

static const char g_cursoron[]      = VT100_CURSORON;
static const char g_cursoroff[]     = VT100_CURSOROFF;
#if 0 /* Not used */
static const char g_cursorhome[]    = VT100_CURSORHOME;
#endif
static const char g_erasetoeol[]    = VT100_CLEAREOL;
#if 0 /* Not used */
static const char g_clrscreen[]     = VT100_CLEARSCREEN;
#endif
static const char g_index[]         = VT100_INDEX;
static const char g_revindex[]      = VT100_REVINDEX;
static const char g_attriboff[]     = VT100_MODESOFF;
static const char g_boldon[]        = VT100_BOLD;
static const char g_reverseon[]     = VT100_REVERSE;
#if 0 /* Not used */
static const char g_blinkon[]       = VT100_BLINK;
static const char g_boldoff[]       = VT100_BOLDOFF;
static const char g_reverseoff[]    = VT100_REVERSEOFF;
static const char g_blinkoff[]      = VT100_BLINKOFF;
#endif

static const char g_fmtcursorpos[]  = VT100_FMT_CURSORPOS;

/* Error format strings */

static const char g_fmtallocfail[]  = "Failed to allocate memory";
static const char g_fmtcmdfail[]    = "%s failed: %d";
static const char g_fmtnotfile[]    = "%s is not a regular file";
static const char g_fmtfileexists[] = "File exists (add ! to override)";
static const char g_fmtmodified[]   =
                            "No write since last change (add ! to override)";
static const char g_fmtnotvalid[]   = "Command not valid";
static const char g_fmtnotcmd[]     = "Not an editor command: %s";
static const char g_fmtsrcbot[]     = "search hit BOTTOM, continuing at TOP";
static const char g_fmtsrctop[]     = "search hit TOP, continuing at BOTTOM";
static const char g_fmtinsert[]     = "--INSERT--";

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Low-level display and data entry functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_write
 *
 * Description:
 *   Write a sequence of bytes to the console device (stdout, fd = 1).
 *
 ****************************************************************************/

static void vi_write(FAR struct vi_s *vi, FAR const char *buffer,
                     size_t buflen)
{
  ssize_t nwritten;
  size_t  nremaining = buflen;

  /* Loop until all bytes have been successfully written (or until a
   * unrecoverable error is encountered)
   */

  do
    {
      /* Take the next gulp */

      nwritten = write(1, buffer, buflen);

      /* Handle write errors.  write() should neve return 0. */

      if (nwritten <= 0)
        {
          /* EINTR is not really an error; it simply means that a signal was
           * received while waiting for write.
           */

          int errcode = errno;
          if (nwritten == 0 || errcode != EINTR)
            {
              fprintf(stderr, "ERROR: write to stdout failed: %d\n",
                      errcode);
              exit(EXIT_FAILURE);
            }
        }

      /* Decrement the count of bytes remaining to be sent (to handle the
       * case of a partial write)
       */

      else
        {
          nremaining -= nwritten;
        }
    }
  while (nremaining > 0);
}

/****************************************************************************
 * Name: vi_putch
 *
 * Description:
 *   Write a single character to the console device.
 *
 ****************************************************************************/

static void vi_putch(FAR struct vi_s *vi, char ch)
{
  vi_write(vi, &ch, 1);
}

/****************************************************************************
 * Name: vi_getch
 *
 * Description:
 *   Get a single character from the console device (stdin, fd = 0).
 *
 ****************************************************************************/

static int vi_getch(FAR struct vi_s *vi)
{
  char buffer;
  ssize_t nread;

  /* Loop until we successfully read a character (or until an unexpected
   * error occurs).
   */

  if (vi->tcurs != NULL)
    {
      int specialkey;
      int modifiers;

      /* Get key from termcurses */

      return termcurses_getkeycode(vi->tcurs, &specialkey, &modifiers);
    }
  else
    {
      do
        {
          /* Read one character from the incoming stream */

          nread = read(0, &buffer, 1);

          /* Check for error or end-of-file. */

          if (nread <= 0)
            {
              /* EINTR is not really an error; it simply means that a signal
               * we received while waiting for input.
               */

              int errcode = errno;
              if (nread == 0 || errcode != EINTR)
                {
                  fprintf(stderr, "ERROR: read from stdin failed: %d\n",
                          errcode);
                  exit(EXIT_FAILURE);
                }
            }
        }
      while (nread < 1);
    }

  /* On success, return the character that was read */

  viinfo("Returning: %c[%02x]\n", isprint(buffer) ? buffer : '.', buffer);
  return buffer;
}

/****************************************************************************
 * Name: vi_clearbottomline
 *
 * Description:
 *   Clear the bottom statusline.
 *
 ****************************************************************************/

static void vi_clearbottomline(FAR struct vi_s *vi)
{
  vi_setcursor(vi, vi->display.row - 1, 0);
  vi_clrtoeol(vi);
}

/****************************************************************************
 * Name: vi_boldon
 *
 * Description:
 *   Enable the blinking attribute at the current cursor location
 *
 ****************************************************************************/

static void vi_boldon(FAR struct vi_s *vi)
{
  /* Send the VT100 BOLDON command */

  vi_write(vi, g_boldon, sizeof(g_boldon));
}

/****************************************************************************
 * Name: vi_reverseon
 *
 * Description:
 *   Enable the blinking attribute at the current cursor location
 *
 ****************************************************************************/

static void vi_reverseon(FAR struct vi_s *vi)
{
  /* Send the VT100 REVERSON command */

  vi_write(vi, g_reverseon, sizeof(g_reverseon));
}

/****************************************************************************
 * Name: vi_attriboff
 *
 * Description:
 *   Disable all previously selected attributes.
 *
 ****************************************************************************/

static void vi_attriboff(FAR struct vi_s *vi)
{
  /* Send the VT100 ATTRIBOFF command */

  vi_write(vi, g_attriboff, sizeof(g_attriboff));
}

/****************************************************************************
 * Name: vi_cursoron
 *
 * Description:
 *   Turn on the cursor
 *
 ****************************************************************************/

static void vi_cursoron(FAR struct vi_s *vi)
{
  /* Send the VT100 CURSORON command */

  vi_write(vi, g_cursoron, sizeof(g_cursoron));
}

/****************************************************************************
 * Name: vi_cursoroff
 *
 * Description:
 *   Turn off the cursor
 *
 ****************************************************************************/

static void vi_cursoroff(FAR struct vi_s *vi)
{
  /* Send the VT100 CURSOROFF command */

  vi_write(vi, g_cursoroff, sizeof(g_cursoroff));
}

/****************************************************************************
 * Name: vi_setcursor
 *
 * Description:
 *   Move the current cursor position to position (row,col)
 *
 ****************************************************************************/

static void vi_setcursor(FAR struct vi_s *vi, uint16_t row, uint16_t column)
{
  char buffer[16];
  int len;

  viinfo("row=%d column=%d\n", row, column);

  /* Format the cursor position command.  The origin is (1,1). */

  len = snprintf(buffer, 16, g_fmtcursorpos, row + 1, column + 1);

  /* Send the VT100 CURSORPOS command */

  vi_write(vi, buffer, len);
}

/****************************************************************************
 * Name: vi_clrtoeol
 *
 * Description:
 *   Clear the display from the current cursor position to the end of the
 *   current line.
 *
 ****************************************************************************/

static void vi_clrtoeol(FAR struct vi_s *vi)
{
  /* Send the VT100 ERASETOEOL command */

  vi_write(vi, g_erasetoeol, sizeof(g_erasetoeol));
}

/****************************************************************************
 * Name: vi_scrollup
 *
 * Description:
 *   Scroll the display up 'nlines' by sending the VT100 INDEX command.
 *
 ****************************************************************************/

static void vi_scrollup(FAR struct vi_s *vi, uint16_t nlines)
{
  viinfo("nlines=%d\n", nlines);

  /* Scroll for the specified number of lines */

  for (; nlines; nlines--)
    {
      /* Send the VT100 INDEX command */

      vi_write(vi, g_index, sizeof(g_index));
    }

  /* Ensure bottom line is cleared */

  vi_setcursor(vi, vi->display.row - 1, 0);
  vi_clrtoeol(vi);
}

/****************************************************************************
 * Name: vi_scrolldown
 *
 * Description:
 *   Scroll the display down 'nlines' by sending the VT100 REVINDEX command.
 *
 ****************************************************************************/

static void vi_scrolldown(FAR struct vi_s *vi, uint16_t nlines)
{
  viinfo("nlines=%d\n", nlines);

  /* Ensure the bottom line is cleared after the scroll */

  vi_setcursor(vi, vi->display.row - 2, 0);
  vi_clrtoeol(vi);

  /* Scroll for the specified number of lines */

  for (; nlines; nlines--)
    {
      /* Send the VT100 REVINDEX command */

      vi_write(vi, g_revindex, sizeof(g_revindex));
    }
}

/****************************************************************************
 * Name: vi_printf
 *
 * Description:
 *   Show a highlighted message at the final line of the display.
 *
 ****************************************************************************/

static void vi_printf(FAR struct vi_s *vi, FAR const char *prefix,
                      FAR const char *fmt, ...)
{
  struct vi_pos_s cursor;
  va_list ap;
  int len;

  /* Save the current cursor position */

  cursor.row    = vi->cursor.row;
  cursor.column = vi->cursor.column;

  /* Set up for a reverse text message on the final line */

  vi_setcursor(vi, vi->display.row - 1, 0);
  vi_reverseon(vi);

  /* Expand the prefix message in the scratch buffer */

  len = prefix ? snprintf(vi->scratch, SCRATCH_BUFSIZE, prefix) : 0;

  va_start(ap, fmt);
  len += vsnprintf(vi->scratch + len, SCRATCH_BUFSIZE - len, fmt, ap);
  vvidbg(fmt, ap);
  va_end(ap);

  /* Write the error message to the display in reverse text */

  vi_write(vi, vi->scratch, len);

  /* Restore normal attributes */

  vi_attriboff(vi);

  /* Reposition the cursor */

  vi_setcursor(vi, cursor.row, cursor.column);

  /* Remember that there is an error message on the last line of the display.
   * When the display is refreshed, the last line will not be altered until
   * the error is cleared.
   */

  vi->error = true;
  VI_BEL(vi);
}

/****************************************************************************
 * Line positioning
 ****************************************************************************/

/****************************************************************************
 * Name: vi_linebegin
 *
 * Description:
 *   Search backward for the beginning of the current line
 *
 ****************************************************************************/

static off_t vi_linebegin(FAR struct vi_s *vi, off_t pos)
{
  /* Search backward to find the previous newline character (or, possibly,
   * the beginning of the text buffer).
   */

  while (pos && vi->text[pos - 1] != '\n')
    {
      pos--;
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Name: vi_prevline
 *
 * Description:
 *   Search backward for the beginning of the previous line
 *
 ****************************************************************************/

static off_t vi_prevline(FAR struct vi_s *vi, off_t pos)
{
  /* Find the beginning the of current line */

  pos = vi_linebegin(vi, pos);

  /* If this not the first line, then back up one more character to position
   * at the last byte of the previous line.
   */

  if (pos > 0)
    {
      pos = vi_linebegin(vi, pos - 1);
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Name: vi_lineend
 *
 * Description:
 *   Search forward for the end of the current line
 *
 ****************************************************************************/

static off_t vi_lineend(FAR struct vi_s *vi, off_t pos)
{
  /* Search forward to find the next newline character. (or, possibly,
   * the end of the text buffer).
   */

  while (pos < vi->textsize && vi->text[pos] != '\n')
    {
      pos++;
    }

  if (vi->text[pos] == '\n')
    {
      pos--;
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Name: vi_nextline
 *
 * Description:
 *   Search backward for the start of the next line
 *
 ****************************************************************************/

static off_t vi_nextline(FAR struct vi_s *vi, off_t pos)
{
  /* Position at the end of the current line */

  pos = vi_lineend(vi, pos) + 1;

  /* If this is not the last byte in the buffer, then increment by one
   * for position of the first byte of the next line.
   */

  if (pos < vi->textsize)
    {
      pos++;
    }

  viinfo("Return pos=%ld\n", (long)pos);
  return pos;
}

/****************************************************************************
 * Text buffer management
 ****************************************************************************/

/****************************************************************************
 * Name: vi_extendtext
 *
 * Description:
 *   Reallocate the in-memory file memory by (at least) 'increment' and make
 *   space for new text of size 'increment' at the specified cursor position.
 *
 ****************************************************************************/

static bool vi_extendtext(FAR struct vi_s *vi, off_t pos, size_t increment)
{
  FAR char *alloc;
  int i;

  viinfo("pos=%ld increment=%ld\n", (long)pos, (long)increment);

  /* Check if we need to reallocate */

  if (!vi->text || vi->textsize + increment > vi->txtalloc)
    {
      /* Allocate in chunksize so that we do not have to reallocate so
       * often.
       */

      size_t allocsize = ALIGN_GULP(vi->textsize + increment);
      alloc = realloc(vi->text, allocsize);
      if (alloc == NULL)
        {
          /* Reallocation failed */

          vi_error(vi, g_fmtallocfail);
          return false;
        }

      /* Save the new buffer information */

      vi->text    = alloc;
      vi->txtalloc = allocsize;
    }

  /* Move text to make space for new text of size 'increment' at the current
   * cursor position
   */

  for (i = vi->textsize - 1; i >= pos; i--)
    {
      vi->text[i + increment] = vi->text[i];
    }

  /* Adjust end of file position */

  vi->textsize += increment;
  vi->modified  = true;
  return true;
}

/****************************************************************************
 * Name: vi_shrinkpos
 *
 * Description:
 *   This is really part of vi_shrinktext.  When any text is deleted, any
 *   positions lying beyond the deleted region in the text buffer must be
 *   adjusted.
 *
 * Input Parameters:
 *   delpos  The position where text was deleted
 *   delsize The number of bytes deleted.
 *   pos     A pointer to a position that may need to be adjusted.
 *
 ****************************************************************************/

static void vi_shrinkpos(FAR struct vi_s *vi, off_t delpos, size_t delsize,
                         FAR off_t *pos)
{
  viinfo("delpos=%ld delsize=%ld pos=%ld\n",
         (long)delpos, (long)delsize, (long)*pos);

  /* Check if the position is beyond the deleted region */

  if (*pos > delpos + delsize)
    {
      /* Yes... just subtract the size of the deleted region */

      *pos -= delsize;
    }

  /* What if the position is within the deleted region?  Set it to the
   * beginning of the deleted region.
   */

  else if (*pos > delpos)
    {
      *pos = delpos;
    }

  /* Ensure the position is within the text bounds in case the
   * text at the end of the buffer is being deleted
   */

  if (*pos >= vi->textsize && vi->mode != MODE_INSERT &&
       vi->mode != MODE_REPLACE)
    {
      *pos = vi->textsize - 1;
    }

  /* Check pos for negative bounds */

  if (*pos < 0)
    {
      *pos = 0;
    }
}

/****************************************************************************
 * Name: vi_shrinktext
 *
 * Description:
 *   Delete a region in the text buffer by copying the end of the text buffer
 *   over the deleted region and adjusting the size of the region.  The text
 *   region may be reallocated in order to recover the unused memory.
 *
 ****************************************************************************/

static void vi_shrinktext(FAR struct vi_s *vi, off_t pos, size_t size)
{
  FAR char *alloc;
  size_t allocsize;
  int i;

  viinfo("pos=%ld size=%ld\n", (long)pos, (long)size);

  /* Close up the gap to remove 'size' characters at 'pos' */

  for (i = pos + size; i < vi->textsize; i++)
    {
      vi->text[i - size] = vi->text[i];
    }

  /* Ensure we are not shrinking more than we have */

  if (size > vi->textsize)
    {
      size = vi->textsize;
    }

  /* Adjust sizes and positions */

  vi->textsize -= size;
  vi->modified  = true;
  vi_shrinkpos(vi, pos, size, &vi->curpos);
  vi_shrinkpos(vi, pos, size, &vi->winpos);
  vi_shrinkpos(vi, pos, size, &vi->prevpos);

  /* Reallocate the buffer to free up memory no longer in use */

  allocsize = ALIGN_GULP(vi->textsize);
  if (allocsize == 0)
    {
      allocsize = TEXT_GULP_SIZE;
    }

  if (allocsize < vi->txtalloc)
    {
      alloc = realloc(vi->text, allocsize);
      if (!alloc)
        {
          vi_error(vi, g_fmtallocfail);
          return;
        }

      /* Save the new buffer information */

      vi->text    = alloc;
      vi->txtalloc = allocsize;
    }
}

/****************************************************************************
 * File access
 ****************************************************************************/

/****************************************************************************
 * Name: vi_insertfile
 *
 * Description:
 *   Insert the contents of a file into the text buffer
 *
 ****************************************************************************/

static bool vi_insertfile(FAR struct vi_s *vi, off_t pos,
                          FAR const char *filename)
{
  struct stat buf;
  FILE *stream;
  off_t filesize;
  size_t nread;
  int result;
  bool ret;

  viinfo("pos=%ld filename=\"%s\"\n", (long)pos, filename);

  /* Get the size of the file  */

  result = stat(filename, &buf);
  if (result < 0)
    {
      vi_message(vi, "\"%s\" [New File]", filename);
      return false;
    }

  /* Check for zero-length file */

  filesize = buf.st_size;
  if (filesize < 1)
    {
      return false;
    }

  /* Open the file for reading */

  stream = fopen(filename, "r");
  if (!stream)
    {
      vi_error(vi, g_fmtcmdfail, "open", errno);
      return false;
    }

  /* [Re]allocate the text buffer to hold the file contents at the current
   * cursor position.
   */

  ret = false;
  if (vi_extendtext(vi, pos, filesize))
    {
      /* Read the contents of the file into the text buffer at the
       * current cursor position.
       */

      nread = fread(vi->text + pos, 1, filesize, stream);
      if (nread < filesize)
        {
          /* Report the error (or partial read), EINTR is not handled */

          vi_error(vi, g_fmtcmdfail, "fread", errno);
          vi_shrinktext(vi, pos, filesize);
        }
      else
        {
          ret = true;
        }
    }

  vi->fullredraw = true;
  fclose(stream);
  return ret;
}

/****************************************************************************
 * Name: vi_savetext
 *
 * Description:
 *   Save a region of the text buffer to 'filename'
 *
 ****************************************************************************/

static bool vi_savetext(FAR struct vi_s *vi, FAR const char *filename,
                        off_t pos, size_t size)
{
  FAR FILE *stream;
  size_t nwritten;
  int len;

  viinfo("filename=\"%s\" pos=%ld size=%ld\n",
         filename, (long)pos, (long)size);

  /* Open the file for writing */

  stream = fopen(filename, "w");
  if (!stream)
    {
      vi_error(vi, g_fmtcmdfail, "fopen", errno);
      return false;
    }

  /* Write the region of the text buffer beginning at pos and extending
   * through pos + size -1.
   */

  nwritten = fwrite(vi->text + pos, 1, size, stream);
  if (nwritten < size)
    {
      /* Report the error (or partial write).  EINTR is not handled. */

      vi_error(vi, g_fmtcmdfail, "fwrite", errno);
      fclose(stream);
      return false;
    }

  fclose(stream);

  len = sprintf(vi->scratch, "%dC written", nwritten);
  vi_write(vi, vi->scratch, len);
  return true;
}

/****************************************************************************
 * Name: vi_checkfile
 *
 * Description:
 *   Check if a file by this name already exists.
 *
 ****************************************************************************/

static bool vi_checkfile(FAR struct vi_s *vi, FAR const char *filename)
{
  struct stat buf;
  int ret;

  viinfo("filename=\"%s\"\n", filename);

  /* Get the size of the file  */

  ret = stat(filename, &buf);
  if (ret < 0)
    {
      /* The file does not exist */

      return false;
    }

  /* It exists, but is it a regular file */

  if (!S_ISREG(buf.st_mode))
    {
      /* Report the error... there is really no good return value in
       * this case.
       */

      vi_error(vi, g_fmtnotfile, filename);
    }

  return true;
}

/****************************************************************************
 * Mode Management Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_setmode
 *
 * Description:
 *   Set the new mode (or command sub-mode) and reset all other common state
 *   variables.  NOTE that a numeric value may be passed to the new mode in
 *   the value field.
 *
 ****************************************************************************/

static void vi_setmode(FAR struct vi_s *vi, uint8_t mode, long value)
{
  viinfo("mode=%d value=%ld\n", mode, value);

  /* Set the mode and clear mode-dependent states that are not preserved
   * across mode changes.
   */

  vi->mode           = mode;
  vi->delarm         = false;
  vi->yankarm        = false;
  vi->toparm         = false;
  vi->chgarm         = false;
  vi->wqarm          = false;
  vi->value          = value;
  vi->cmdlen         = 0;
}

/****************************************************************************
 * Name: vi_setsubmode
 *
 * Description:
 *   Set up one of the data entry sub-modes of the command mode.  These are
 *   modes in which commands or search data will be entered on the final line
 *   of the display.
 *
 ****************************************************************************/

static void vi_setsubmode(FAR struct vi_s *vi, uint8_t mode, char prompt,
                          long value)
{
  viinfo("mode=%d prompt='%c' value=%ld\n", mode, prompt, value);

  /* Set up the new mode */

  vi_setmode(vi, mode, value);

  /* Save the previous cursor position (not required by all modes) */

  vi->cursave.row    = vi->cursor.row;
  vi->cursave.column = vi->cursor.column;

  /* Set up for data entry on the final line */

  vi->cursor.row     = vi->display.row - 1;
  vi->cursor.column  = 0;
  vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

  /* Output the prompt character in bold text */

  vi_boldon(vi);
  vi_putch(vi, prompt);
  vi_attriboff(vi);

  /* Clear to the end of the line */

  vi_clrtoeol(vi);

  /* Update the cursor position */

  vi->cursor.column  = 1;
}

/****************************************************************************
 * Name:  vi_exitsubmode
 *
 * Description:
 *   Exit the data entry sub-mode and return to normal command mode.
 *
 ****************************************************************************/

static void vi_exitsubmode(FAR struct vi_s *vi, uint8_t mode)
{
  viinfo("mode=%d\n", mode);

  /* Set up the new mode */

  vi_setmode(vi, mode, 0);

  /* Restore the saved cursor position */

  vi->cursor.row    = vi->cursave.row;
  vi->cursor.column = vi->cursave.column;
}

/****************************************************************************
 * Display Management
 ****************************************************************************/

/****************************************************************************
 * Name: vi_windowpos
 *
 * Description:
 *   Based on the position of the cursor in the text buffer, determine the
 *   horizontal display cursor position, performing TAB expansion as
 *   necessary.
 *
 ****************************************************************************/

static void vi_windowpos(FAR struct vi_s *vi, off_t start, off_t end,
                         uint16_t *pcolumn, off_t *ppos)
{
  uint16_t column;
  off_t pos;

  viinfo("start=%ld end=%ld\n", (long)start, (long)end);

  /* Make sure that the end position is not beyond the end of the text. We
   * assume that the start position is okay.
   */

  if (end > vi->textsize)
    {
      end = vi->textsize;
    }

  /* Loop incrementing the text buffer position while text buffer position
   * is within range.
   */

  for (pos = start, column = 0; pos < end && (column < vi->reqcolumn ||
        vi->updatereqcol); pos++)
    {
      /* Is there a newline terminator at this position? */

      if (vi->text[pos] == '\n')
        {
          /* Yes... break out of the loop return the cursor column */

          if (vi->mode != MODE_INSERT && vi->mode != MODE_REPLACE &&
              pos != start)
            {
              pos--;
              column--;
            }

          break;
        }

      /* No... Is there a TAB at this position? */

      else if (vi->text[pos] == '\t')
        {
          /* Yes.. expand the TAB */

          column = NEXT_TAB(column);
        }

      /* No, then just increment the cursor column by one character */

      else
        {
          column++;
        }
    }

  /* Keep cursor in bounds of text (i.e. not at the '\n') */

  if (((pos == vi->textsize && column != 0) ||
       (vi->text[pos] == '\n' && pos != start)) &&
        vi->mode != MODE_INSERT && vi->mode != MODE_REPLACE)
    {
      pos--;
      column--;
    }

  /* Now return the requested values */

  if (ppos)
    {
      *ppos = pos;
    }

  if (pcolumn)
    {
      *pcolumn = column;
    }

  if (vi->updatereqcol)
    {
      vi->reqcolumn = column;
      vi->updatereqcol = false;
    }
}

/****************************************************************************
 * Name: vi_scrollcheck
 *
 * Description:
 *   Check if any operations will require that we scroll the display.
 *
 ****************************************************************************/

static void vi_scrollcheck(FAR struct vi_s *vi)
{
  off_t curline;
  off_t pos;
  uint16_t tmp;
  int column;
  int nlines;

  /* Sanity test */

  if (vi->curpos > vi->textsize)
    {
      vi->curpos = vi->textsize;
    }

  /* Get the text buffer offset to the beginning of the current line */

  curline = vi_linebegin(vi, vi->curpos);

  /* Check if the current line is above the first line on the display */

  while (curline < vi->winpos)
    {
      /* Yes.. move the window position up to the beginning of the previous
       * line line and check again
       */

      vi->winpos = vi_prevline(vi, vi->winpos);
      vi->vscroll--;
      vi->fullredraw = true;
    }

  /* Reset the cursor row position so that it is relative to the
   * top of the display.
   */

  vi->cursor.row = 0;
  for (pos = vi->winpos; pos < curline; pos = vi_nextline(vi, pos))
    {
      vi->cursor.row++;
    }

  /* Check if the cursor row position is below the bottom of the display */

  for (; vi->cursor.row >= vi->display.row - 1; vi->cursor.row--)
    {
      /* Yes.. move the window position down by one line and check again */

      vi->winpos = vi_nextline(vi, vi->winpos);
      vi->vscroll++;
      vi->fullredraw = true;
    }

  /* Check if the cursor column is on the display.  vi_windowpos returns the
   * unrestricted column number of cursor.  hscroll is the horizontal offset
   * in characters.
   */

  vi_windowpos(vi, curline, vi->curpos, &tmp, NULL);
  column = (int)tmp - (int)vi->hscroll;

  /* Force the cursor column to lie on the display.  First check if the
   * column lies to the left of the horizontal scrolling position.  If it
   * does, move the scroll position to the left by tabs until the cursor
   * lies on the display.
   */

  while (column < 0)
    {
      column      += VI_TABSIZE;
      vi->hscroll -= VI_TABSIZE;
      vi->fullredraw = true;
    }

  /* If the cursor column lies to the right of the display, then adjust
   * the horizontal scrolling position so that the cursor position does
   * lie on the display.
   */

  while (column >= vi->display.column)
    {
      column      -= VI_TABSIZE;
      vi->hscroll += VI_TABSIZE;
      vi->fullredraw = true;
    }

  /* That final adjusted position is the display cursor column */

  vi->cursor.column = column;

  /* Check if new window position is below the previous position.
   * In this case, we will need to scroll up until the new window
   * position is at the top of the display.
   */

  if (vi->winpos > vi->prevpos)
    {
      /* We will need to scroll up.  Count how many lines we
       * need to scroll.
       */

      for (nlines = 0, pos = vi->prevpos;
           pos != vi->winpos && nlines < vi->display.row - 1;
           nlines++)
        {
          pos = vi_nextline(vi, pos);
        }

      /* Then scroll up that number of lines */

      if (nlines < vi->display.row - 1)
        {
          vi_scrollup(vi, nlines);
          vi->fullredraw = true;
        }
    }

  /* Check if new window position is above the previous position.
   * In this case, we will need to scroll down until the new window
   * position is at the top of the display.
   */

  else if (vi->winpos < vi->prevpos)
    {
      for (nlines = 0, pos = vi->prevpos;
           pos != vi->winpos && nlines < vi->display.row - 1;
           nlines++)
        {
          pos = vi_prevline(vi, pos);
        }

      /* Then scroll down that number of lines */

      if (nlines < vi->display.row - 1)
        {
          vi_scrolldown(vi, nlines);
          vi->fullredraw = true;
        }
    }

  /* Save the previous top-of-display position for next time around.
   * This can be modified asynchronously by text deletion operations.
   */

  vi->prevpos = vi->winpos;
  viinfo("winpos=%ld hscroll=%d\n",
         (long)vi->winpos, (long)vi->hscroll);
}

/****************************************************************************
 * Name: vi_showtext
 *
 * Description:
 *   Update the display based on the last operation.  This function is
 *   called at the beginning of the processing loop in Command and Insert
 *   modes (and also in the continuous replace mode).
 *
 ****************************************************************************/

static void vi_showtext(FAR struct vi_s *vi)
{
  off_t pos;
  off_t writefrom;
  uint16_t row;
  uint16_t endrow;
  uint16_t column;
  uint16_t endcol;
  uint16_t tabcol;
  bool redraw_line;

  /* Check if any of the preceding operations will cause the display to
   * scroll.
   */

  vi_scrollcheck(vi);

  /* If no display updates needed after scrollcheck, just return */

  if (!vi->fullredraw && !vi->drawtoeos && !vi->redrawline)
    {
      return;
    }

  /* If there is an error message at the bottom of the display, then
   * do not update the last line.
   */

  endrow = vi->display.row - 1;

  /* Make sure that all character attributes are disabled; Turn off the
   * cursor during the update.
   */

  vi_attriboff(vi);
  vi_cursoroff(vi);

  /* Set loop control variables based on draw mode */

  if (vi->fullredraw)
    {
      /* Start from beginning of display */

      pos = vi->winpos;
      row = 0;

      /* Ensure drawtoeos and redraw line are also set */

      vi->drawtoeos  = true;
      vi->redrawline = true;
    }
  else
    {
      /* Start drawing from current row */

      pos = vi_linebegin(vi, vi->curpos);
      row = vi->cursor.row;

      if (vi->drawtoeos)
        {
          vi->redrawline = true;
        }
      else
        {
          endrow = row + 1;
        }
    }

  /* Write each line to the display, handling horizontal scrolling and
   * tab expansion.
   */

  for (; pos < vi->textsize && row < endrow; row++)
    {
      /* Test if this line needs to be redrawn */

      redraw_line = true;
      if (!vi->redrawline)
        {
          redraw_line = false;
        }
      else if (row + 1 < vi->cursor.row && !vi->fullredraw)
        {
          redraw_line = false;
        }
      else if (row > vi->cursor.row && !vi->drawtoeos)
        {
          redraw_line = false;
        }
      else if (row == vi->cursor.row && !vi->redrawline)
        {
          redraw_line = false;
        }

      /* Get the last column on this row.  Avoid writing into the last byte
       * on the screen which may trigger a scroll.
       */

      endcol = vi->display.column;
      if (row >= vi->display.row - 1)
        {
         endcol--;
        }

      /* Get the position into this line corresponding to display column 0,
       * accounting for horizontal scrolling and tab expansion.  Add that to
       * the line start offset to get the first offset to consider for
       * display.
       */

      vi_windowpos(vi, pos, pos + vi->hscroll, NULL, &pos);

      /* Set the cursor position to the beginning of this row and clear to
       * the end of the line.
       */

      if (redraw_line)
        {
          vi_setcursor(vi, row, 0);

          /* Loop for each column */

          writefrom = pos;
          for (column = 0; pos < vi->textsize && column < endcol; pos++)
            {
              /* Break out of the loop if we encounter the newline before the
               * last column is encountered.
               */

              if (vi->text[pos] == '\n')
                {
                  break;
                }

              /* Perform TAB expansion */

              else if (vi->text[pos] == '\t')
                {
                  /* Write collected characters */

                  if (writefrom != pos)
                    {
                      vi_write(vi, &vi->text[writefrom], pos - writefrom);
                    }

                  tabcol = NEXT_TAB(column);
                  if (tabcol < endcol)
                    {
                      for (; column < tabcol; column++)
                        {
                          vi_putch(vi, ' ');
                        }

                      writefrom = pos + 1;
                    }
                  else
                    {
                      /* Break out of the loop... there is nothing left on
                       * the line but whitespace.
                       */

                      writefrom = pos;
                      break;
                    }
                }

              /* Add the normal character to the display */

              else
                {
                  column++;
                }
            }

          /* Write collected characters */

          if (writefrom != pos)
            {
              vi_write(vi, &vi->text[writefrom], pos - writefrom);
            }

          vi_clrtoeol(vi);
        }

      /* Skip to the beginning of the next line */

      pos = vi_nextline(vi, pos);
    }

  if (pos == vi->textsize && vi->text[pos - 1] == '\n')
    {
      vi_setcursor(vi, row, 0);
      vi_clrtoeol(vi);
      row++;
    }

  /* If we are drawing to EOS, then draw trailing '~' */

  if (vi->drawtoeos)
    {
      /* If there was not enough text to fill the display, clear the
       * remaining lines (except for any possible error line at the
       * bottom of the display).
       */

      for (; row < endrow; row++)
        {
          /* Set the cursor position to the beginning of the row and clear to
           * the end of the line.
           */

          vi_setcursor(vi, row, 0);
          if (row != endrow && row != 0)
            {
              vi_putch(vi, '~');
            }

          vi_clrtoeol(vi);
        }
    }

  /* Turn the cursor back on */

  vi_cursoron(vi);
  vi->fullredraw = false;
  vi->drawtoeos = false;
  vi->redrawline = false;
}

/****************************************************************************
 * Name: vi_showlinecol
 *
 * Description:
 *   Update the current line/column on the display status line.
 *
 ****************************************************************************/

static void vi_showlinecol(FAR struct vi_s *vi)
{
  size_t len;

  /* Move to bototm line for display */

  vi_cursoroff(vi);
  vi_setcursor(vi, vi->display.row - 1, vi->display.column - 15);

  len = snprintf(vi->scratch, SCRATCH_BUFSIZE, "%jd,%d",
                 (uintmax_t)(vi->cursor.row + vi->vscroll + 1),
                 vi->cursor.column + vi->hscroll + 1);
  vi_write(vi, vi->scratch, len);

  vi_clrtoeol(vi);
  vi_cursoron(vi);
}

/****************************************************************************
 * Command Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_cusorup
 *
 * Description:
 *   Move the cursor up one line in the text buffer.
 *
 ****************************************************************************/

static void vi_cusorup(FAR struct vi_s *vi, int nlines)
{
  int remaining;
  off_t start;
  off_t end;

  viinfo("nlines=%d\n", nlines);

  /* How many lines do we need to move?  Zero means 1 (so does 1) */

  remaining = (nlines < 1 ? 1 : nlines);

  /* Get the offset to the start of the current line */

  start = vi_linebegin(vi, vi->curpos);

  /* Now move the cursor position back the correct number of lines */

  for (; remaining > 0; remaining--)
    {
      /* Get the start position of the previous line */

      start = vi_prevline(vi, start);

      /* Find the cursor position on the next line corresponding to the
       * cursor position on the current line.
       */

      end = start + vi->reqcolumn;
      vi_windowpos(vi, start, end, NULL, &vi->curpos);
    }
}

/****************************************************************************
 * Name: vi_cursordown
 *
 * Description:
 *   Move the cursor down one line in the text buffer
 *
 ****************************************************************************/

static void vi_cursordown(FAR struct vi_s *vi, int nlines)
{
  int remaining;
  off_t start;
  off_t end;

  viinfo("nlines=%d\n", nlines);

  /* How many lines do we need to move?  Zero means 1 (so does 1) */

  remaining = (nlines < 1 ? 1 : nlines);

  /* Get the offset to the start of the current line */

  start = vi_linebegin(vi, vi->curpos);

  /* Now move the cursor position forward the correct number of lines */

  for (; remaining > 0; remaining--)
    {
      /* Get the start of the next line. */

      start = vi_nextline(vi, start);

      /* Find the cursor position on the next line corresponding to the
       * cursor position on the current line.
       */

      end = start + vi->reqcolumn;
      vi_windowpos(vi, start, end, NULL, &vi->curpos);
    }
}

/****************************************************************************
 * Name: vi_cursorleft
 *
 * Description:
 *   Move the cursor left 'ncolumns' columns in the text buffer (without
 *   moving to the preceding line).  Note that a repetition count of 0 means
 *  to  perform the movement once.
 *
 ****************************************************************************/

static off_t vi_cursorleft(FAR struct vi_s *vi, off_t curpos, int ncolumns)
{
  int remaining;

  viinfo("curpos=%ld ncolumns=%d\n", curpos, ncolumns);

  /* Loop decrementing the cursor position for each repetition count.  Break
   * out early if we hit either the beginning of the text buffer, or the end
   * of the previous line.
   */

  for (remaining = (ncolumns < 1 ? 1 : ncolumns);
       curpos > 0 && remaining > 0 && vi->text[curpos - 1] != '\n';
       curpos--, remaining--)
    {
    }

  return curpos;
}

/****************************************************************************
 * Name: vi_cursorright
 *
 * Description:
 *   Move the cursor right 'ncolumns' columns in the text buffer (without
 *   moving to the next line).  Note that a repetition count of 0 means to
 *   perform the movement once.
 *
 ****************************************************************************/

static off_t vi_cursorright(FAR struct vi_s *vi, off_t curpos, int ncolumns)
{
  int remaining;

  viinfo("curpos=%ld ncolumns=%d\n", curpos, ncolumns);

  /* Loop incrementing the cursor position for each repetition count.  Break
   * out early if we hit either the end of the text buffer, or the end of
   * the line.
   */

  for (remaining = (ncolumns < 1 ? 1 : ncolumns);
       curpos < vi->textsize && remaining > 0 && vi->text[curpos] != '\n';
       curpos++, remaining--)
    {
    }

#if 0
  if (vi->text[curpos] == '\n' || (curpos == vi->textsize &&
      vi->mode != MODE_INSERT && vi->mode != MODE_REPLACE))
    {
      curpos--;
    }
#endif

  return curpos;
}

/****************************************************************************
 * Name: vi_gotoscreenbottom
 *
 * Description:
 *   Move the cursor to the bottom of the screen or the bottom line of
 *   the file if it doesn't occupy the entire screen.
 *
 ****************************************************************************/

static int vi_gotoscreenbottom(FAR struct vi_s *vi, int rows)
{
  off_t pos;
  int row;
  int target = rows > 0 ? rows >> 1 : vi->display.row - 2;

  vi->curpos = vi->winpos;
  row = 0;
  while (row < target)
    {
      /* Get position of next row down */

      pos = vi_nextline(vi, vi->curpos);

      /* Test for end of file before bottom of screen */

      if (pos == vi->curpos)
        {
          break;
        }

      if (pos == vi->textsize)
        {
          /* Test for empty line at the bottom */

          row++;
          vi->curpos = pos;
          break;
        }

      vi->curpos = pos;
      row++;
    }

  /* Report the location of the bottom row */

  return row;
}

/****************************************************************************
 * Name: vi_gotofirstnonwhite
 *
 * Description:
 *   Move the cursor to the first non-whitespace character on the
 *   current line.
 *
 ****************************************************************************/

static void vi_gotofirstnonwhite(FAR struct vi_s *vi)
{
  vi->curpos = vi_linebegin(vi, vi->curpos);
  while (vi->curpos <= vi->textsize && (vi->text[vi->curpos] == ' ' ||
         vi->text[vi->curpos] == '\t'))
    {
      vi->curpos++;
    }
}

/****************************************************************************
 * Name: vi_delforward
 *
 * Description:
 *   Delete characters from the current cursor position forward
 *
 ****************************************************************************/

static void vi_delforward(FAR struct vi_s *vi)
{
  off_t start;
  off_t end;
  bool at_end = false;

  viinfo("curpos=%ld value=%ld\n", (long)vi->curpos, vi->value);

  /* Test for empty file */

  if (vi->textsize == 0)
    {
      return;
    }

  /* Test for empty line deletion and simply return */

  if (vi->cursor.column == 0)
    {
      /* If at end of file, just return */

      if (vi->curpos == vi->textsize ||
          vi->text[vi->curpos] == '\n')
        {
          return;
        }
    }

  /* Get the cursor position as if we would have move the cursor right N
   * times (which might be <N characters).
   */

  end  = vi_cursorright(vi, vi->curpos, vi->value);

  /* Test for deletion at end of the line */

  if (end == vi->curpos && vi->cursor.column > 0)
    {
      end++;
      at_end = true;
    }

  /* The difference from the current position then is the number of
   * characters to be deleted.
   */

  start = vi->curpos;

  vi_yanktext(vi, start, end - 1, true, true);
  vi->curpos = start;
  if (at_end)
    {
      vi->curpos--;
    }

  vi->redrawline = true;
}

/****************************************************************************
 * Name: vi_delbackward
 *
 * Description:
 *   Delete characters before the current cursor position
 *
 ****************************************************************************/

static void vi_delbackward(FAR struct vi_s *vi)
{
  off_t start;
  off_t end;
  off_t x;

  viinfo("curpos=%ld value=%ld\n", (long)vi->curpos, vi->value);

  /* Test if we are at beginning of line */

  if (vi->curpos == 0 || vi->text[vi->curpos] == '\n' ||
      vi->text[vi->curpos - 1] == '\n')
    {
      return;
    }

  /* Back up one character.  This is where the deletion will end */

  end = vi_cursorleft(vi, vi->curpos, 1);

  /* Get the cursor position as if we would have move the cursor left N
   * times (which might be <N characters).
   */

  if (vi->value > 1)
    {
      start = vi_cursorleft(vi, end, vi->value -1);
    }
  else
    {
      start = end;
    }

  for (x = end; x >= start; x--)
    {
      /* Test if \n' in the range.  Don't delete through \n */

      if (vi->text[x] == '\n')
        {
          start = x + 1;
          break;
        }
    }

  /* The difference from the current position then is the number of
   * characters to be deleted.
   */

  vi_yanktext(vi, start, end, true, true);
  vi->redrawline = true;
}

/****************************************************************************
 * Name: vi_linerange
 *
 * Description:
 *   Return the start and end positions for N lines in the text buffer,
 *   beginning at the current line.  This is logic common to yanking and
 *   deleting lines.
 *
 ****************************************************************************/

static void vi_linerange(FAR struct vi_s *vi, off_t *start, off_t *end)
{
  off_t next;
  int nlines;

  /* Get the offset in the text buffer to the beginning of the current line */

  *start = vi_linebegin(vi, vi->curpos);

  /* Move one line unless a repetition count was provided */

  nlines = (vi->value > 0 ? vi->value : 1);

  /* Search ahead to find the end of the last line to yank */

  for (next = *start; nlines > 1; nlines--)
    {
      next = vi_nextline(vi, next);
    }

  *end = vi_lineend(vi, next);
  if (*end != vi->textsize)
    {
      (*end)++;
    }
}

/****************************************************************************
 * Name: vi_delline
 *
 * Description:
 *   Delete N lines from the text buffer, beginning at the current line.
 *
 ****************************************************************************/

static void vi_delline(FAR struct vi_s *vi)
{
  /* Yank and remove text from the buffer */

  vi_yank(vi, true);
  vi->drawtoeos = true;
}

/****************************************************************************
 * Name: vi_deltoeol
 *
 * Description:
 *   Delete to end of line.
 *
 ****************************************************************************/

static void vi_deltoeol(FAR struct vi_s *vi)
{
  int start;
  int end;

  /* If we are at the end of the line, then return */

  if (vi->curpos == vi->textsize || vi->text[vi->curpos] == '\n')
    {
      return;
    }

  /* Determine start and end location */

  start = vi->curpos;
  end   = vi_lineend(vi, vi->curpos);
  if (end == vi->textsize || vi->text[end] == '\n')
    {
      end--;
    }

  /* Yank and remove text from the buffer */

  vi_yanktext(vi, start, end, true, true);
  if (start > 0 && start != vi->textsize && vi->text[start - 1] != '\n')
    {
      vi->curpos = start - 1;
    }
  else
    {
      vi->curpos = start;
    }

  vi->redrawline = true;
}

/****************************************************************************
 * Name: vi_yanktext
 *
 * Description:
 *   Yank specified text from the text buffer and delete if requested.
 *
 ****************************************************************************/

static void vi_yanktext(FAR struct vi_s *vi, off_t start, off_t end,
                        bool yankcharmode, bool del_after_yank)
{
  int append_lf = 0;
  size_t alloc;
  size_t size;

  /* At end of file, in line yank mode, if there is no LF, we append one */

  if (vi->text[end] != '\n' && !yankcharmode)
    {
      append_lf = 1;
    }

  /* Allocate a yank buffer big enough to hold the lines */

  size  = end - start + 1;
  alloc = size + append_lf;

  if (alloc < CONFIG_SYSTEM_VI_YANK_THRESHOLD)
    {
      alloc = CONFIG_SYSTEM_VI_YANK_THRESHOLD;
    }

  /* Free any previously yanked lines */

  if (vi->yank)
    {
      /* Free the buffer only if it is too small or if it is larger
       * than the YANK_THRESHOLD and we need less than that.
       */

      if (alloc > vi->yankalloc ||
          (alloc == CONFIG_SYSTEM_VI_YANK_THRESHOLD &&
           vi->yankalloc > CONFIG_SYSTEM_VI_YANK_THRESHOLD))
        {
          free(vi->yank);
          vi->yank = NULL;
        }
    }

  /* Allocate buffer if not already allocated */

  if (!vi->yank)
    {
      vi->yankalloc    = alloc;
      vi->yank         = (FAR char *)malloc(vi->yankalloc);
    }

  vi->yankcharmode = yankcharmode;

  if (!vi->yank)
    {
      vi_error(vi, g_fmtallocfail);
      vi->yankalloc = 0;
      vi->yanksize = 0;
      return;
    }

  /* Copy the block from the text buffer to the yank buffer */

  vi->yanksize = size;
  memcpy(vi->yank, &vi->text[start], size);

  /* Append \n if needed */

  if (append_lf > 0)
    {
      vi->yank[vi->yanksize] = '\n';
    }

  /* Remove the yanked text from the text buffer */

  if (del_after_yank)
    {
      vi_shrinktext(vi, start, vi->yanksize);
    }

  /* Account for appended lf in yankalloc */

  vi->yanksize += append_lf;
}

/****************************************************************************
 * Name: vi_yank
 *
 * Description:
 *   Remove N lines from the text buffer, beginning at the current line and
 *   copy the lines to an allocated yank buffer.
 *
 ****************************************************************************/

static void vi_yank(FAR struct vi_s *vi, bool del_after_yank)
{
  off_t start;
  off_t end;
  off_t yank_end;
  off_t textsize;
  int pos_increment = 0;
  bool empty_last_line = false;

  /* Get the offset in the text buffer corresponding to the range of lines to
   * be yanked
   */

  vi_linerange(vi, &start, &end);
  textsize = vi->textsize;

  /* Do end of file bounds checking */

  if (end >= textsize)
    {
      end = textsize - 1;
    }

  if (start >= textsize)
    {
      start = textsize -1;
    }

  /* When yanking last line with \n, don't delete the \n */

  yank_end = end;
  if (del_after_yank && end == textsize - 1 && start != end &&
      vi->text[end] == '\n')
    {
      yank_end--;
      pos_increment = 1;
    }

  viinfo("start=%ld end=%ld\n", (long)start, (long)end);

  /* Test if deleting last line with empty line above it */

  if ((end > 0 && start == end && end == vi->textsize -1 &&
      vi->text[end - 1] == '\n') || (start > 1 && end + 1 ==
      vi->textsize && vi->text[start - 2] == '\n'))
    {
      empty_last_line = true;
    }

  vi_yanktext(vi, start, yank_end, 0, del_after_yank);

  /* If the last line was yanked, then remove the '\n' on the
   * previous line.
   */

  if (end + 1 == textsize && start != end && del_after_yank)
    {
      vi_shrinktext(vi, vi->textsize - 1, 1);
    }

  /* Place cursor at beginning of the line */

  if (del_after_yank)
    {
      if (empty_last_line)
        {
          vi->curpos = vi->textsize;
        }
      else
        {
          vi->curpos = vi_linebegin(vi, vi->curpos + pos_increment);
        }
    }
}

/****************************************************************************
 * Name: vi_paste
 *
 * Description:
 *   Copy line(s) from the yank buffer, and past them after the current line.
 *   The contents of the yank buffer are released.
 *
 ****************************************************************************/

static void vi_paste(FAR struct vi_s *vi, bool paste_before)
{
  off_t start;
  off_t new_curpos;
  int count;

  viinfo("curpos=%ld yankalloc=%d\n", (long)vi->curpos, (long)vi->yankalloc);

  /* Make sure there is something to be pasted */

  if (!vi->yank || vi->yanksize <= 0)
    {
      return;
    }

  /* Get the command count */

  count = vi->value > 0 ? vi->value : 1;
  if (count > 1)
    {
      vi->fullredraw = true;
    }

  /* Test for char mode paste buffer */

  while (count > 0)
    {
      if (vi->yankcharmode)
        {
          off_t pos;

          /* Paste at next col to the right of cursor */

          if (vi->text[vi->curpos] == '\n' || vi->curpos == vi->textsize ||
              paste_before)
            {
              pos = vi->curpos;
            }
          else
            {
              pos = vi->curpos + 1;
            }

          if (vi_extendtext(vi, pos, vi->yanksize))
            {
              /* Copy the contents of the yank buffer into the text buffer
               * at the position where the start of the next line was.
               */

              memcpy(&vi->text[pos], vi->yank, vi->yanksize);

              /* Advance the cursor */

              vi->curpos = vi->curpos + vi->yanksize;
              if (vi->curpos > vi->textsize || vi->text[vi->curpos] == '\n')
                {
                  vi->curpos--;
                }
            }
        }
      else
        {
          off_t   size;

          /* Paste at the beginning of the next line */

          if (paste_before)
            {
              start = vi_linebegin(vi, vi->curpos);
            }
          else
            {
              start = vi_nextline(vi, vi->curpos);
            }

          size = vi->yanksize;

          /* Test if pasting at end of file */

          new_curpos = start;
          if ((start >= vi->textsize && vi->text[vi->textsize - 1] != '\n')
              || vi->curpos == vi->textsize)
            {
              off_t textsize = vi->textsize;
              bool at_end = vi->curpos == vi->textsize;

              vi->curpos = vi->textsize;
              vi_insertch(vi, '\n');
              start      = vi->textsize;
              new_curpos = start;

              /* Don't append the \n' in the yank buffer */

              if (vi->text[textsize - 1] != '\n' || at_end)
                {
                  size--;
                }
            }

          /* Ensure start <= textsize */

          else if (start >= vi->textsize)
            {
              start          = vi->textsize;
              new_curpos     = start;
              vi->fullredraw = true;
            }

          /* Reallocate the text buffer to hold the yank buffer contents at
           * the beginning of the next line.
           */

          if (vi_extendtext(vi, start, size))
            {
              /* Copy the contents of the yank buffer into the text buffer
               * at the position where the start of the next line was.
               */

              memcpy(&vi->text[start], vi->yank, size);

              /* Advance to next line */

              vi->curpos = new_curpos;
            }
        }

      count--;
    }

  /* Redraw everything below this point */

  vi->drawtoeos = true;
}

/****************************************************************************
 * Name: vi_join
 *
 * Description:
 *   Join line below with current line.
 *
 ****************************************************************************/

static void vi_join(FAR struct vi_s *vi)
{
  off_t start;
  off_t end;

  /* Test if we are at end of file */

  if (vi->curpos + 1 >= vi->textsize)
    {
      return;
    }

  start = vi_lineend(vi, vi->curpos);

  /* Ensure the line ends with '\n' */

  if (vi->text[start + 1] != '\n')
    {
      return;
    }

  /* Convert the '\n' to a space */

  vi->text[++start] = ' ';
  end = start + 1;

  /* Skip all spaces and tabs on next line */

  while ((vi->text[end] == ' ' || vi->text[end] == '\t') &&
      end < vi->textsize)
    {
      end++;
    }

  if (start + 1 != end)
    {
      vi_shrinktext(vi, start + 1, end - (start + 1));
    }

  vi->curpos    = start;
  vi->drawtoeos = true;
}

/****************************************************************************
 * Name: vi_gotoline
 *
 * Description:
 *   Position the cursor at the line specified by vi->value.  If
 *   vi->value is zero, then the cursor is position at the end of the text
 *   buffer.
 *
 ****************************************************************************/

static void vi_gotoline(FAR struct vi_s *vi)
{
  viinfo("curpos=%ld value=%ld\n", (long)vi->curpos, vi->value);

  /* Special case the first line */

  if (vi->value == 1)
    {
      vi->curpos = 0;
    }

  /* Work harder to position to lines in the middle */

  else if (vi->value > 0)
    {
      uint32_t line;

      /* Got to the line == value */

      for (line = vi->value, vi->curpos = 0;
          --line > 0 && vi->curpos < vi->textsize;
          )
        {
          vi->curpos = vi_nextline(vi, vi->curpos);
        }
    }

  /* No value means to go to beginning of the last line */

  else
    {
      /* Get the beginning of the last line */

      vi->curpos = vi_linebegin(vi, vi->textsize);
    }

  vi->fullredraw = true;
}

/****************************************************************************
 * Name: vi_chartype
 *
 * Description:
 *   Determine and return the type of character (i.e. alpha, space,
 *   punctuation).
 *
 ****************************************************************************/

static int vi_chartype(char ch)
{
  int type;

  if (ch == ' ' || ch == '\t')
    {
      type = VI_CHAR_SPACE;
    }

  /* Test for alpha, numeric or '_' */

  else if (isalnum(ch) || ch == '_')
    {
      type = VI_CHAR_ALPHA;
    }

  /* Test for CR or NL */

  else if (ch == '\r' || ch == '\n')
    {
      type = VI_CHAR_CRLF;
    }

  /* Must be punctuation */

  else
    {
      type = VI_CHAR_PUNCT;
    }

  return type;
}

/****************************************************************************
 * Name: vi_findnextword
 *
 * Description:
 *   Find the position in the text buffer of the start of the next word.
 *
 ****************************************************************************/

static off_t vi_findnextword(FAR struct vi_s *vi)
{
  int srch_type;
  int pos_type;
  off_t pos;

  /* Get the type of character under the cursor so we know what the
   * next "word" looks like.
   */

  srch_type = vi_chartype(vi->text[vi->curpos]);
  pos = vi->curpos + 1;

  for (; pos < vi->textsize; pos++)
    {
      /* Get type of the next character */

      pos_type = vi_chartype(vi->text[pos]);

      /* Skip CR and NL */

      if (pos_type == VI_CHAR_CRLF)
        {
          /* Change to search type SPACE */

          srch_type = VI_CHAR_SPACE;
          continue;
        }

      /* We we were over a space, then any non-space is the next word */

      if (srch_type == VI_CHAR_SPACE &&
          pos_type != VI_CHAR_SPACE)
        {
          break;
        }

      /* Test for next punctuation if search type is alpha */

      if (srch_type == VI_CHAR_ALPHA &&
          pos_type == VI_CHAR_PUNCT)
        {
          break;
        }

      /* Test for next alpha if search type is punctuation */

      if (srch_type == VI_CHAR_PUNCT &&
          pos_type == VI_CHAR_ALPHA)
        {
          break;
        }

      /* Test for alpha search followed by space. Then switch the search type
       * to space so we find whatever is next.
       */

      if ((srch_type == VI_CHAR_ALPHA || srch_type == VI_CHAR_PUNCT) &&
          pos_type == VI_CHAR_SPACE)
        {
          srch_type = VI_CHAR_SPACE;
        }
    }

  /* Limit position to within the valid text range */

  if (pos == vi->textsize)
    {
      pos--;
    }

  /* Return the position of the next word */

  return pos;
}

/****************************************************************************
 * Name: vi_gotonextword
 *
 * Description:
 *   Position the cursor at the start of the next word.
 *
 ****************************************************************************/

static void vi_gotonextword(FAR struct vi_s *vi)
{
  int count;
  int x;
  off_t start = vi->curpos;
  off_t end;
  off_t pos;
  bool crfound;

  /* Loop for the specified search count */

  count = vi->value > 0 ? vi->value : 1;
  for (x = 0; x < count; x++ )
    {
      /* Get position of next word */

      vi->curpos = vi_findnextword(vi);
    }

  /* Test if yank,  delete or change are armed */

  if (vi->yankarm || vi->delarm || vi->chgarm)
    {
      /* Rewind so we don't yank skipped whitespace */

      pos     = vi->curpos;
      crfound = false;

      while ((vi->text[pos - 1] == ' ' || vi->text[pos - 1] == '\t' ||
             vi->text[pos - 1] == '\n') && pos > start)
        {
          /* We rewind only if '\n' found before non-space */

          pos--;
          if (vi->text[pos] == '\n')
            {
              crfound = true;
            }
        }

      if (crfound)
        {
          vi->curpos = pos;
        }

      /* If the yank / delete count is 1, then limit the yank/delete so it
       * doesn't contain any '\n' characters.
       */

      if (count == 1)
        {
          /* Scan the text range and look for '\n' */

          for (x = start; x < vi->curpos; x++)
            {
              /* Test for '\n' */

              if (vi->text[x] == '\n')
                {
                  /* Modify the yank / delete range */

                  vi->curpos = x;
                  break;
                }
            }
        }
      else
        {
          /* Multi-line delete? */

          vi->fullredraw = vi->delarm || vi->chgarm;
        }

      /* Perform the yank */

      end = vi->curpos + 1 == vi->textsize ? vi->curpos : vi->curpos - 1;
      if (vi->chgarm)
        {
          end--;
        }

      /* Yank text if it isn't a single \n character */

      if (!(start == end && vi->text[start] == '\n'))
        {
          vi_yanktext(vi, start, end, 1, vi->delarm | vi->chgarm);
        }

      if (vi->delarm | vi->chgarm)
        {
          /* Redraw line if text deleted */

          vi->redrawline = true;
        }

      /* Restore the original curpos */

      vi->curpos = start;

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
      /* Setup command repeat */

      if (vi->delarm | vi->chgarm)
        {
          vi_saverepeat(vi, vi->delarm ? 'd' : 'c');
          vi_appendrepeat(vi, 'w');
        }
#endif

      /* If change text is armed, then enter insert mode */

      if (vi->chgarm)
        {
          vi_setmode(vi, MODE_INSERT, 0);
        }

      vi->delarm = false;
      vi->chgarm = false;
      vi->yankarm = false;
    }
}

/****************************************************************************
 * Name: vi_findprevword
 *
 * Description:
 *   Find the position in the text buffer of the start of the previous word.
 *
 ****************************************************************************/

static off_t vi_findprevword(FAR struct vi_s *vi)
{
  int srch_type;
  int pos_type;
  off_t pos;

  /* If we are basically at the beginning of the file, then the task
   * is simple.
   */

  if (vi->curpos < 2)
    {
      return 0;
    }

  /* Get the type of character under the cursor so we know what the
   * next "word" looks like.
   */

  srch_type = vi_chartype(vi->text[vi->curpos]);
  pos       = vi->curpos - 1;
  pos_type  = vi_chartype(vi->text[pos]);

  /* Test if we are at the beginning of a word */

  if (srch_type == pos_type)
    {
      /* Not at beginning of word.  Find beginning of word. */

      while (pos > 0)
        {
          pos_type = vi_chartype(vi->text[pos - 1]);

          if (pos_type != srch_type && pos_type != VI_CHAR_CRLF)
            {
              break;
            }

          pos--;
        }

      if ((srch_type != VI_CHAR_SPACE && srch_type != VI_CHAR_CRLF) ||
          pos == 0)
        {
          return pos;
        }

      /* We found the start of a string of spaces.  Decrement to first
       * non-space character.
       */

      pos_type = vi_chartype(vi->text[--pos]);
    }

  /* If the previous char is space, then skip them */

  while ((pos_type == VI_CHAR_SPACE || pos_type == VI_CHAR_CRLF) && pos > 0)
    {
      pos_type = vi_chartype(vi->text[--pos]);
    }

  if (pos == 0)
    {
      return pos;
    }

  /* Now find beginning of this new type */

  srch_type = vi_chartype(vi->text[pos]);
  while (pos > 0 && vi_chartype(vi->text[pos - 1]) == srch_type)
    {
      pos--;
    }

  /* Return the position of the next word */

  return pos;
}

/****************************************************************************
 * Name: vi_gotoprevword
 *
 * Description:
 *   Position the cursor at the start of the previous word.
 *
 ****************************************************************************/

static void vi_gotoprevword(FAR struct vi_s *vi)
{
  int count;

  /* Loop for the specified search count */

  count = vi->value > 0 ? vi->value : 1;
  while (count > 0)
    {
      /* Get position of next word */

      vi->curpos = vi_findprevword(vi);
      count--;
    }
}

/****************************************************************************
 * Name: vi_bottom_line_debug
 *
 * Description:
 *   Print text and paste buffers on bottom line for debug purposes
 *
 ****************************************************************************/

#ifdef ENABLE_BOTTOM_LINE_DEBUG
static void vi_bottom_line_debug(FAR struct vi_s *vi)
{
  off_t pos;
  int column;

  vi_clearbottomline(vi);

  vi_putch(vi, '"');
  pos    = 0;
  column = 0;

  while (pos < vi->textsize && column < vi->display.column)
    {
      if (vi->text[pos] == '\n')
        {
          vi_putch(vi, '\\');
          vi_putch(vi, 'n');
        }
      else if (vi->text[pos] == '\t')
        {
          vi_putch(vi, '\\');
          vi_putch(vi, 'n');
        }
      else
        {
          vi_putch(vi, vi->text[pos]);
        }

      pos++;
      column++;
    }

  vi_putch(vi, '"');
  if (!vi->yank)
    {
      return;
    }

  vi_putch(vi, ' ');
  vi_putch(vi, '"');
  pos = 0;

  while (pos < vi->yanksize && column < vi->display.column)
    {
      if (vi->yank[pos] == '\n')
        {
          vi_putch(vi, '\\');
          vi_putch(vi, 'n');
        }
      else if (vi->yank[pos] == '\t')
        {
          vi_putch(vi, '\\');
          vi_putch(vi, 'n');
        }
      else
        {
          vi_putch(vi, vi->yank[pos]);
        }

      pos++;
    }

  vi_putch(vi, '"');
}
#endif /* ENABLE_BOTTOM_LINE_DEBUG */

/****************************************************************************
 * Name: vi_findnext
 *
 * Description:
 *   Perform find operation again in forward direction
 *
 ****************************************************************************/

void vi_findnext(FAR struct vi_s *vi)
{
  if (vi->curpos < vi->textsize)
    {
      vi->curpos++;
    }

  /* Search for string */

  if (!vi_findstring(vi))
    {
      /* Restore original pos if not found */

      vi->curpos--;
      VI_BEL(vi);
    }
}

/****************************************************************************
 * Name: vi_findprev
 *
 * Description:
 *   Perform find operation again in reverse direction
 *
 ****************************************************************************/

void vi_findprev(FAR struct vi_s *vi)
{
  off_t pos = vi->curpos;

  /* Move the cursor to the left */

  if (vi->curpos > 0)
    {
      vi->curpos--;
    }
  else
    {
      vi->curpos = vi->textsize - strlen(vi->findstr);
    }

  /* Perform the search */

  if (!vi_revfindstring(vi))
    {
      /* Restore the position if not found */

      vi->curpos = pos;
    }
}

/****************************************************************************
 * Name: vi_saverepeat
 *
 * Description:
 *   Save ch as the first command repeat entry.
 *
 ****************************************************************************/

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
static void vi_saverepeat(FAR struct vi_s *vi, uint16_t ch)
{
  /* If we are in command repeat mode, then don't initialize */

  if (vi->cmdrepeat)
    {
      return;
    }

  vi->cmdcount = 0;
  vi->cmdindex = 0;

  if (ch < 256)
    {
      vi->cmdbuf[vi->cmdcount++] = ch;
      vi->repeatvalue = vi->value;
    }
}
#endif /* CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT */

/****************************************************************************
 * Name: vi_appendrepeat
 *
 * Description:
 *   Save ch as the next command repeat entry.
 *
 ****************************************************************************/

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
static void vi_appendrepeat(FAR struct vi_s *vi, uint16_t ch)
{
  /* If we are in command repeat mode, then don't append */

  if (vi->cmdrepeat || vi->cmdcount == 0 || ch > 255)
    {
      return;
    }

  /* Don't overflow the command repeat buffer */

  if (vi->cmdcount < CMD_BUFSIZE)
    {
      vi->cmdbuf[vi->cmdcount++] = ch;
    }
}
#endif /* CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT */

/****************************************************************************
 * Name: vi_cmd_mode
 *
 * Description:
 *   Command mode loop
 *
 ****************************************************************************/

static void vi_cmd_mode(FAR struct vi_s *vi)
{
  viinfo("Enter command mode\n");

  /* Loop while we are in command mode */

  while (vi->mode == MODE_COMMAND)
    {
      bool preserve;
      int ch;

      /* Make sure that the display reflects the current state */

      vi_showtext(vi);
      vi_showlinecol(vi);
      vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

      /* Get the next character from the input */

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
      /* Test for end of command repeat */

      if (vi->cmdrepeat && vi->cmdindex == vi->cmdcount)
        {
          /* Terminate the command repeat */

          vi->cmdrepeat = false;
        }

      /* Test for active cmdrepeat */

      if (vi->cmdrepeat)
        {
          /* Read next command from command buffer */

          ch = vi->cmdbuf[vi->cmdindex++];
        }
      else
#endif
        {
          ch = vi_getch(vi);
        }

      /* Handle numeric input.  Zero (0) with no preceding value is a
       * special case: It means to go to the beginning o the line.
       */

      if (isdigit(ch) && (vi->value > 0 || ch != '0'))
        {
          uint32_t tmp = 10 * vi->value + (ch - '0');
          if (tmp > UINT16_MAX)
            {
              tmp = UINT16_MAX;
            }

          /* Update the command repetition count */

          vi->value = tmp;

          viinfo("Value=%ld\n", vi->value);
          continue;
        }

      /* Allow the following during yank / delete modes */

      if (ch != 'f' && ch != 't' && ch != 'w')
        {
          /* Anything other than 'd' disarms line deletion */

          if (ch != 'd')
            {
              vi->delarm = false;
            }

          /* Anything other than 'y' disarms line yanking */

          if (ch != 'y')
            {
              vi->yankarm = false;
            }

          /* Anything other than 'c' disarms line yanking */

          if (ch != 'c')
            {
              vi->chgarm = false;
            }
        }

      /* Anything other than'g' disarms goto top */

      if (ch != 'g')
        {
          vi->toparm = false;
        }

      /* Anything other than'Z' disarms :wq */

      if (ch != 'Z')
        {
          vi->wqarm = false;
        }

      /* Test for empty file */

      if (vi->textsize == 0)
        {
          /* We need some text before we can do anything.  Only accept
           * text insertion commands.
           */

          if (ch != KEY_CMDMODE_APPEND    && ch != KEY_CMDMODE_INSERT    &&
              ch != KEY_CMDMODE_OPENBELOW && ch != KEY_CMDMODE_APPENDEND &&
              ch != KEY_CMDMODE_INSBEGIN  && ch != KEY_CMDMODE_OPENABOVE &&
              ch != KEY_CMDMODE_COLONMODE)
            {
              continue;
            }
        }

      /* Any key press clears the error message */

      vi->error = false;

      /* Then handle the non-numeric character.  Normally the accumulated
       * value will be reset after processing the command.  There are a few
       * exceptions; 'preserve' will be set to 'true' in those exceptional
       * cases.
       */

      preserve = false;
      vi->updatereqcol = true;
      switch (ch)
        {
        case KEY_CMDMODE_UP: /* Move the cursor up one line */
        case KEY_UP:         /* Move the cursor up one line */
          {
            vi->updatereqcol = false;
            vi_cusorup(vi, vi->value);
          }
          break;

        case KEY_CMDMODE_DOWN: /* Move the cursor down one line */
        case KEY_DOWN:         /* Move the cursor down one line */
          {
            vi->updatereqcol = false;
            vi_cursordown(vi, vi->value);
          }
          break;

        case KEY_CMDMODE_LEFT: /* Move the cursor left N characters */
        case KEY_LEFT:         /* Move the cursor left N characters */
          {
            vi->curpos = vi_cursorleft(vi, vi->curpos, vi->value);
          }
          break;

        case KEY_CMDMODE_RIGHT: /* Move the cursor right one character */
        case KEY_RIGHT:         /* Move the cursor right one character */
          {
            if (vi->text[vi->curpos] != '\n' &&
                vi->text[vi->curpos + 1] != '\n')
              {
                vi->curpos = vi_cursorright(vi, vi->curpos, vi->value);
                if (vi->curpos >= vi->textsize)
                  {
                    vi->curpos = vi->textsize - 1;
                  }
              }
          }
          break;

        case KEY_CMDMODE_BEGINLINE: /* Move cursor to start of current line */
        case KEY_HOME:
          {
            vi->curpos = vi_linebegin(vi, vi->curpos);
          }
          break;

        case KEY_CMDMODE_ENDLINE: /* Move cursor to end of current line */
        case KEY_END:
          {
            vi->curpos = vi_lineend(vi, vi->curpos);
            vi->reqcolumn = 65535;
            vi->updatereqcol = false;
          }
          break;

        case KEY_CMDMODE_PAGEUP: /* Move up (backward) one screen */
        case KEY_PPAGE:
          {
            vi->updatereqcol = false;
            vi_cusorup(vi, vi->display.row);
          }
          break;

        case KEY_CMDMODE_PAGEDOWN: /* Move down (forward) one screen */
        case KEY_NPAGE:
          {
            vi->updatereqcol = false;
            vi_cursordown(vi, vi->display.row);
          }
          break;

        case KEY_CMDMODE_HALFUP: /* Move up (backward) one screen */
          {
            vi->updatereqcol = false;
            vi_cusorup(vi, vi->display.row >> 1);
          }
          break;

        case KEY_CMDMODE_HALFDOWN: /*  Move down (forward) one half screen */
          {
            vi->updatereqcol = false;
            vi_cursordown(vi, vi->display.row >> 1);
          }
          break;

        case KEY_CMDMODE_TOP:     /* Move to top of screen */
          {
            vi->curpos = vi->winpos;
          }
          break;

        case KEY_CMDMODE_BOTTOM:  /* Move to bottom of screen */
          {
            vi_gotoscreenbottom(vi, 0);
          }
          break;

        case KEY_CMDMODE_MIDDLE:  /* Move to middle of screen */
          {
            /* Find bottom row number, then move to half that */

            off_t pos = vi_gotoscreenbottom(vi, 0);
            vi_gotoscreenbottom(vi, pos);
          }
          break;

        case KEY_CMDMODE_FIRSTCHAR:
          {
            vi_gotofirstnonwhite(vi);
          }
          break;

        case KEY_CMDMODE_GOTOTOP: /* Go to top of document */
          {
            if (vi->toparm)
              {
                vi->curpos = 0;
                vi->redrawline = true;
                vi->toparm = false;
              }
            else
              {
                vi->toparm = true;
              }
          }
          break;

        case KEY_CMDMODE_FINDNEXT:
          {
            if (vi->revfind)
              {
                vi_findprev(vi);
              }
            else
              {
                vi_findnext(vi);
              }
            break;
          }

        case KEY_CMDMODE_FINDPREV:
          {
            if (vi->revfind)
              {
                vi_findnext(vi);
              }
            else
              {
                vi_findprev(vi);
              }
            break;
          }
          break;

        case ASCII_BS:  /* Delete N characters before the cursor */
          {
            /* Move the cursor to the left */

            if (vi->curpos > 0)
              {
                vi->curpos--;

                /* If we moved to \n on the previous line, skip it */

                if (vi->curpos > 0 && vi->text[vi->curpos] == '\n')
                  {
                    vi->curpos--;
                  }
              }
          }
          break;

        case KEY_CMDMODE_DEL_LINE: /* Delete the current line */
          {
            if (vi->delarm)
              {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
                vi_saverepeat(vi, ch);
                vi_appendrepeat(vi, ch);
#endif
                vi_delline(vi);
                vi->delarm = false;
              }
            else
              {
                vi->delarm = true;
                preserve = true;
              }
          }
          break;

        case KEY_CMDMODE_CHANGE:
          {
            if (vi->chgarm)
              {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
                vi_saverepeat(vi, ch);
                vi_appendrepeat(vi, ch);
#endif
                vi_gotofirstnonwhite(vi);
                vi_deltoeol(vi);
                vi_setmode(vi, MODE_INSERT, 0);
                if (vi->curpos == vi->textsize)
                  {
                    vi->curpos = vi_cursorright(vi, vi->curpos, 1) + 1;
                  }
                else
                  {
                    vi->curpos = vi_cursorright(vi, vi->curpos, 1);
                  }

                vi->chgarm = false;
              }
            else
              {
                vi->chgarm = true;
                preserve = true;
              }
          }
          break;

        case KEY_CMDMODE_DELTOEOL:  /* Delete to end of current line */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_deltoeol(vi);
          }
          break;

        case KEY_CMDMODE_DELBACKWARD:  /* Delete from cursor forward */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_delbackward(vi);
          }
          break;

        case KEY_CMDMODE_YANK:  /* Yank the current line(s) into the buffer */
          {
            if (vi->yankarm)
              {
                vi_yank(vi, false);
                vi->yankarm = false;
              }
            else
              {
                vi->yankarm = true;
                preserve = true;
              }
          }
          break;

        case KEY_CMDMODE_PASTE: /* Paste line(s) from into text after current line */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_paste(vi, false);
          }
          break;

        case KEY_CMDMODE_PASTEBEFORE: /* Paste text before cursor position */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_paste(vi, true);
          }
          break;

        case KEY_CMDMODE_REPLACECH: /* Replace character(s) under cursor */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_setmode(vi, SUBMODE_REPLACECH, vi->value);
            preserve = true;
          }
          break;

        case KEY_CMDMODE_REPLACE: /* Replace character(s) under cursor until ESC */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_setmode(vi, MODE_REPLACE, 0);
          }
          break; /* Not implemented */

        case KEY_CMDMODE_FINDINLINE:  /* Find character(s) in current line */
        case KEY_CMDMODE_TFINDINLINE: /* Find character(s) in current line */
          {
            vi->tfind = ch == KEY_CMDMODE_TFINDINLINE;
            vi->mode = MODE_FINDINLINE;
            preserve = true;
          }
          break;

        case KEY_CMDMODE_OPENBELOW: /* Enter insertion mode in new line below current */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_setmode(vi, MODE_INSERT, 0);

            /* Go forward to the end of the current line */

            vi->curpos = vi_lineend(vi, vi->curpos);
            if (vi->curpos != vi->textsize)
              {
                /* Include the '\n' */

                vi->curpos++;
              }

            /* Insert a newline to break the line.  The cursor now points
             * beginning of the new line.
             */

            vi_insertch(vi, '\n');

            /* Then enter insert mode */

            vi->drawtoeos = true;
          }
          break;

        case KEY_CMDMODE_OPENABOVE: /* Enter insertion mode in new line above current */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            /* Back up to the beginning of the end of the previous line */

            off_t pos  = vi_linebegin(vi, vi->curpos);
            if (pos == 0)
              {
                /* Insert newline at beginning of file, then move to previous
                 * line.
                 */

                vi->curpos = 0;
                vi_insertch(vi, '\n');
                vi->curpos = vi_prevline(vi, vi->curpos);
              }
            else
              {
                /* Insert a newline to open the line.  The cursor will now
                 * point to thebeginning of newly openly line before the
                 * current line.
                 */

                pos = vi_prevline(vi, pos);
                vi->curpos = vi_lineend(vi, pos)+1;
                vi_insertch(vi, '\n');
              }

            /* Then enter insert mode */

            vi_setmode(vi, MODE_INSERT, 0);
            vi->drawtoeos = true;
          }
          break;

        case KEY_CMDMODE_CHANGETOEOL:  /* Delete to end of current line */
          {
            /* First delete to end of line */

            vi_deltoeol(vi);
          }

          /* Now enter insert mode by falling through the case */

        case KEY_CMDMODE_APPEND: /* Enter insertion mode after the current
                                  * cursor position */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_setmode(vi, MODE_INSERT, 0);

            if (vi->curpos == vi->textsize)
              {
                vi->curpos = vi_cursorright(vi, vi->curpos, 1) + 1;
              }
            else
              {
                vi->curpos = vi_cursorright(vi, vi->curpos, 1);
              }
          }
          break;

        case KEY_CMDMODE_APPENDEND: /* Enter insertion mode at the end of the current line */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_setmode(vi, MODE_INSERT, 0);
            vi->curpos = vi_lineend(vi, vi->curpos) + 1;
          }
          break;

        case KEY_CMDMODE_SUBSTITUTE:
        case KEY_CMDMODE_DEL: /* Delete N characters at the cursor */
        case KEY_DC:
        case ASCII_DEL:
          {
            off_t pos = vi->curpos;

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            /* If we are at the end of the line, then delete backward */

            if (vi->text[pos] == '\n')
              {
                /* Nothing to do */

                break;
              }
            else if (pos + 1 != vi->textsize && vi->text[pos + 1] == '\n')
              {
                if (pos > 0)
                  {
                    vi_delforward(vi);
                    vi->curpos = vi_cursorleft(vi, vi->curpos, 1);
                  }
              }
            else
              {
                vi_delforward(vi);
                vi->redrawline = true;
              }
          }

          /* For 's'ubstitute key, we go into insert mode */

          if (ch == KEY_CMDMODE_SUBSTITUTE)
            {
              vi_setmode(vi, MODE_INSERT, 0);
            }

          break;

        case KEY_CMDMODE_INSBEGIN: /* Enter insertion mode at the beginning of the current line */
          {
            vi->curpos = vi_linebegin(vi, vi->curpos);
          }

          /* Fall through */

        case KEY_CMDMODE_INSERT: /* Enter insertion mode before the current cursor position */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_setmode(vi, MODE_INSERT, 0);
          }
          break;

        case KEY_CMDMODE_JOIN:  /* Join line below with current line */
          {
#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
            vi_saverepeat(vi, ch);
#endif
            vi_join(vi);
          }
          break;

        case KEY_CMDMODE_COLONMODE: /* Enter : command sub-mode */
          {
            vi->updatereqcol = false;
            vi_setsubmode(vi, SUBMODE_COLON, ':', 0);
          }
          break;

        case KEY_CMDMODE_SAVEQUIT:  /* Two of these is the same as :wq */
          {
            if (vi->wqarm)
              {
                /* Emulate :wq */

                strcpy(vi->scratch, "wq");
                vi->cmdlen = 2;
                vi_parsecolon(vi);

                /* If save quit succeeds, we won't return */
              }
            else
              {
                vi->wqarm = true;
              }
          }
          break;

        case KEY_CMDMODE_FINDMODE: /* Enter / find sub-mode */
          {
            vi->updatereqcol = false;
            vi_setsubmode(vi, SUBMODE_FIND, '/', 0);
          }
          break;

        case KEY_CMDMODE_REVFINDMODE: /* Enter / find sub-mode */
          {
            vi->updatereqcol = false;
            vi_setsubmode(vi, SUBMODE_REVFIND, '?', 0);
          }
          break;

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
        case KEY_CMDMODE_REPEAT: /* Repeat the last command */
          {
            if (vi->cmdcount < CMD_BUFSIZE)
              {
                vi->cmdindex = 0;
                vi->cmdrepeat = true;
                vi->value = vi->value > 0 ? vi->value : vi->repeatvalue > 0 ?
                            vi->repeatvalue : 1;
                preserve = true;
              }
            else
              {
                VI_BEL(vi);
              }
          }
          break;
#endif

        case KEY_CMDMODE_GOTO:  /* Go to line specified by the accumulated value */
          {
            vi_gotoline(vi);
          }
          break;

        case KEY_CMDMODE_WORDFWD: /* Go to line specified by the accumulated value */
          {
            vi_gotonextword(vi);
          }
          break;

        case KEY_CMDMODE_WORDBACK: /* Go to line specified by the accumulated value */
          {
            vi_gotoprevword(vi);
          }
          break;

#if defined(CONFIG_EOL_IS_CR)
        case KEY_CMDMODE_NEXTLINE:
        case '\r': /* CR terminates line */
          {
            vi->curpos = vi_nextline(vi, vi->curpos);
            vi_gotofirstnonwhite(vi);
          }
          break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
       case '\r': /* Wait for the LF */
          break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
        case KEY_CMDMODE_NEXTLINE:
        case '\n': /* LF terminates line */
          {
            vi->curpos = vi_nextline(vi, vi->curpos);
            vi_gotofirstnonwhite(vi);
          }
          break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
        case KEY_CMDMODE_NEXTLINE:
        case '\r': /* Either CR or LF terminates line */
        case '\n':
          {
            vi->curpos = vi_nextline(vi, vi->curpos);
            vi_gotofirstnonwhite(vi);
          }
          break;
#endif

        case KEY_CMDMODE_PREVLINE:
          {
            vi->curpos = vi_prevline(vi, vi->curpos);
            vi_gotofirstnonwhite(vi);
          }
          break;

        /* Unimplemented and invalid commands */

        case KEY_CMDMODE_REDRAW:  /* Redraws the screen */
        case KEY_CMDMODE_REDRAW2: /* Redraws the screen, removing deleted lines */
        case KEY_CMDMODE_MARK:    /* Place a mark beginning at the current cursor position */
        default:
          {
            if (ch == -1)
              {
                continue;
              }
            else
              {
                VI_BEL(vi);
              }
          }
          break;
        }

      /* Any non-numeric input will reset the accumulated value (after it has
       * been used).  There are a few exceptions:
       *
       * - For the double character sequences, we need to retain the value
       *   until the next character is entered.
       * - If we are changing modes, then we may need to preserve the 'value'
       *   as well; in some cases settings are passed to the new mode in
       *   'value' (vi_setmode() will have set or cleared 'value'
       *   appropriately).
       */

      if (!preserve)
        {
          vi->value = 0;
        }
    }
}

/****************************************************************************
 * Common Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_cmdch
 *
 * Description:
 *   Insert one character into the data entry line
 *
 ****************************************************************************/

static void vi_cmdch(FAR struct vi_s *vi, char ch)
{
  int index = vi->cmdlen;
  int next  = index + 1;

  viinfo("cmdlen=%d ch=%c[%02x]\n", vi->cmdlen, isprint(ch) ? ch : '.', ch);

  /* Abort gracelessly if the scratch buffer becomes full */

  if (next >= SCRATCH_BUFSIZE)
    {
      vi_exitsubmode(vi, MODE_COMMAND);
      return;
    }

  /* Add the new character to the scratch buffer */

  vi->scratch[index] = ch;
  vi->cmdlen = next;

  /* Update the cursor position */

  vi->cursor.column = next + 1;

  /* And add the new character to the display */

  vi_putch(vi, ch);
  if (ch == '\n')
    {
      vi->drawtoeos = true;
    }
}

/****************************************************************************
 * Name:  vi_cmdbackspace
 *
 * Description:
 *   Process a backspace character in the data entry line
 *
 ****************************************************************************/

static void vi_cmdbackspace(FAR struct vi_s *vi)
{
  viinfo("cmdlen=%d\n", vi->cmdlen);

  if (vi->cmdlen > 0)
    {
      vi_setcursor(vi, vi->display.row - 1, vi->cmdlen);
      vi_clrtoeol(vi);

      /* Update the command index and cursor position */

      vi->cursor.column = vi->cmdlen;

      /* Decrement the number of characters on in the command */

      vi->cmdlen--;
    }
}

/****************************************************************************
 * Colon Data Entry Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name:  vi_parsecolon
 *
 * Description:
 *   Parse the colon command collected in the scratch buffer
 *
 ****************************************************************************/

static void vi_parsecolon(FAR struct vi_s *vi)
{
  FAR const char *filename = NULL;
  uint8_t cmd = CMD_NONE;
  bool done = false;
  bool forced;
  int col;
  int ch;

  viinfo("Parse: \"%s\"\n", vi->scratch);

  /* NUL terminate the command */

  vi->scratch[vi->cmdlen] = '\0';

  /* Convert "wq" into "qw" */

  if (vi->cmdlen > 1 && vi->scratch[0] == KEY_COLMODE_WRITE &&
      vi->scratch[1] == KEY_COLMODE_QUIT)
    {
      vi->scratch[0] = KEY_COLMODE_QUIT;
      vi->scratch[1] = KEY_COLMODE_WRITE;
    }

  /* Then parse the contents of the scratch buffer */

  for (col = 0; col < vi->cmdlen && !done; col++)
    {
      /* Get the next command character from the scratch buffer */

      ch = vi->scratch[col];

      /* Check if the next after that is KEY_COLMODE_FORCE */

      forced = false;
      if (col < vi->cmdlen &&  vi->scratch[col + 1] == KEY_COLMODE_FORCE)
        {
          /* Yes.. the operation is forced */

          forced = true;
          col++;
        }

      /* Then process the command character */

      switch (ch)
        {
          case KEY_COLMODE_READ:
            {
              /* Reading a file should not be forced */

              if (cmd == CMD_NONE && !forced)
                {
                  cmd = CMD_READ;
                }
              else
                {
                  /* The read operation is not compatible with writing or
                   * quitting
                   */

                  goto errout_bad_command;
                }
            }
            break;

          case KEY_COLMODE_WRITE:
            {
              /* Are we just writing?  Or writing then quitting? */

              if (cmd == CMD_NONE)
                {
                  /* Just writing.. do we force overwriting? */

                  cmd = (forced ? CMD_OWRITE : CMD_WRITE);
                }
              else if (cmd == CMD_QUIT)
                {
                  /* Both ... do we force overwriting the file? */

                  cmd = (forced ? CMD_OWRITE_QUIT : CMD_WRITE_QUIT);
                }
              else
                {
                  /* Anything else,
                   * including a forced quit is a syntax error
                   */

                  goto errout_bad_command;
                }
            }
            break;

          case KEY_COLMODE_QUIT:
            {
              /* Are we just quitting?  Or writing then quitting? */

              if (cmd == CMD_NONE)
                {
                  /* Just quitting... should we discard any changes? */

                  cmd = (forced ? CMD_DISCARD : CMD_QUIT);
                }

              /* If we are also writing, then it makes no sense to force the
               * quit operation.
               */

              else if (cmd == CMD_WRITE && !forced)
                {
                  cmd = CMD_WRITE_QUIT;
                }
              else if (cmd == CMD_OWRITE && !forced)
                {
                  cmd = CMD_OWRITE_QUIT;
                }
              else
                {
                  /* Quit is not compatible with reading */

                  goto errout_bad_command;
                }
            }
            break;

          default:
            {
              /* Ignore whitespace */

              if (ch != ' ')
                {
                  /* Anything else terminates the loop */

                  done = true;

                  /* If there is anything else on the line, then it must be
                   * a file name.  If we are writing (or reading with an
                   * empty text buffer), then we will need to copy the file
                   * into the filename storage area.
                   */

                  if (ch != '\0')
                    {
                      /* For now, just remember where the file is in the
                       * scratch buffer.
                       */

                      filename = &vi->scratch[col];
                    }
                }
            }
            break;
        }
    }

  /* Did we find any valid command?  A read command requires a filename.
   * A filename where one is not needed is also an error.
   */

  viinfo("cmd=%d filename=\"%s\"\n", cmd, vi->filename);

  if (cmd == CMD_NONE || (IS_READ(cmd) && !filename) ||
      (!USES_FILE(cmd) && filename))
    {
      goto errout_bad_command;
    }

  /* Are we writing to a new filename?  If we are not forcing the write,
   * then we have to check if the file exists.
   */

  if (filename && IS_NOWRITE(cmd))
    {
      /* Check if the file exists */

      if (vi_checkfile(vi, filename))
        {
          /* It does... show an error and exit */

          vi_error(vi, g_fmtfileexists);
          goto errout;
        }
    }

  /* Check if we are trying to quit with un-saved changes.  The user must
   * force quitting in this case.
   */

  if (vi->modified && IS_NDISCARD(cmd))
    {
      /* Show an error and exit */

      vi_error(vi, g_fmtmodified);
      goto errout;
    }

  /* Are we now committed to reading the file? */

  if (IS_READ(cmd))
    {
      /* Was the text buffer empty? */

      bool empty = (vi->textsize == 0);

      /* Yes.. get the cursor position of the beginning of the next line */

      off_t pos = vi_nextline(vi, vi->curpos);

      /* Then read the file into the text buffer at that position. */

      if (vi_insertfile(vi, pos, filename))
        {
          /* Was the text buffer empty before we inserted the file? */

          if (empty)
            {
              /* Yes.. then we want to save the filename and mark the text
               * as unmodified.
               */

              strncpy(vi->filename, filename, MAX_FILENAME - 1);

             /* Make sure that the (possibly truncated) file name is NUL
              * terminated
              */

              vi->filename[MAX_FILENAME - 1] = '\0';
              vi->modified = false;
            }
          else
            {
              /* No.. then we want to retain the filename and mark the text
               * as modified.
               */

              vi->modified = true;
            }
        }
    }

  /* Are we now committed to writing the file? */

  if (IS_WRITE(cmd))
    {
      /* If we are writing to a new file, then we need to copy the filename
       * from the scratch buffer to the filename buffer.
       */

      if (filename)
        {
          strncpy(vi->filename, filename, MAX_FILENAME - 1);

         /* Make sure that the (possibly truncated) file name is NUL
          * terminated
          */

          vi->filename[MAX_FILENAME - 1] = '\0';
        }

      /* If it is not a new file and if there are no changes to the text
       * buffer, then ignore the write.
       */

      if (filename || vi->modified)
        {
          vi_clearbottomline(vi);
          vi_putch(vi, '"');
          vi_write(vi, vi->filename, strlen(vi->filename));
          vi_putch(vi, '"');
          vi_putch(vi, ' ');

          /* Now, finally, we can save the file */

          if (!vi_savetext(vi, vi->filename, 0, vi->textsize))
            {
              /* An error occurred while saving the file and we are
               * not forcing the quit operation.  So error out without
               * quitting until the user decides what to do about
               * the save failure.
               */

              goto errout;
            }

          /* The text buffer contents are no longer modified */

          if (!IS_QUIT(cmd))
            {
              vi_setcursor(vi, vi->cursor.row, vi->cursor.column);
            }

          vi->modified = false;
        }
    }

  /* Are we committed to exit-ing? */

  if (IS_QUIT(cmd))
    {
      /* Yes... free resources and exit */

      vi_putch(vi, '\n');
      vi_release(vi);
      exit(EXIT_SUCCESS);
    }

  /* Otherwise, revert to command mode */

  vi_exitsubmode(vi, MODE_COMMAND);
  return;

errout_bad_command:
  vi_error(vi, g_fmtnotcmd, vi->scratch);

errout:
  vi_exitsubmode(vi, MODE_COMMAND);
}

/****************************************************************************
 * Name: vi_cmd_submode
 *
 * Description:
 *   Colon command sub-mode of the command mode processing
 *
 ****************************************************************************/

static void vi_cmd_submode(FAR struct vi_s *vi)
{
  int ch;

  viinfo("Enter colon command sub-mode\n");

  /* Loop while we are in colon command mode */

  while (vi->mode == SUBMODE_COLON)
    {
      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_COLMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              vi_cmdch(vi, vi_getch(vi));
            }
            break;

          case ASCII_BS: /* Delete the character(s) before the cursor */
            {
              if (vi->cmdlen == 0)
                {
                  vi_exitsubmode(vi, MODE_COMMAND);

                  /* Ensure bottom line is cleared */

                  vi_clearbottomline(vi);
                }
              else
                {
                  vi_cmdbackspace(vi);
                }
            }
            break;

          case ASCII_ESC: /* Escape exits colon mode */
            {
              vi_exitsubmode(vi, MODE_COMMAND);
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              vi_parsecolon(vi);
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              vi_parsecolon(vi);
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              vi_parsecolon(vi);
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters */

              if (isprint(ch))
                {
                  /* Insert the filtered character into the scratch buffer */

                  vi_cmdch(vi, ch);
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }
}

/****************************************************************************
 * Find Data Entry Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_findstring
 *
 * Description:
 *   Find the string in the findstr buffer by searching for a matching
 *   sub-string in the text buffer, starting at the current cursor position.
 *
 ****************************************************************************/

static bool vi_findstring(FAR struct vi_s *vi)
{
  off_t pos;
  int len;

  viinfo("findstr: \"%s\"\n", vi->findstr);

  /* The search string is in the find buffer */

  len = strlen(vi->findstr);
  if (!len)
    {
      return false;
    }

  /* Search from the current cursor position forward for a
   * matching sub-string.  Stop loo
   */

  vi_clearbottomline(vi);
  for (pos = vi->curpos;
       pos + len <= vi->textsize;
       pos++)
    {
      /* Check for the matching sub-string */

      if (strncmp(vi->text + pos, vi->scratch, len) == 0)
        {
          /* Found it... save the cursor position and
           * return success.
           */

          vi->curpos = pos;
          return true;
        }
    }

  /* If we get here, then the search string was not found anywhere after the
   * current cursor position.  Start from beginning and search to curpos.
   */

  for (pos = 0; pos <= vi->curpos; pos++)
    {
      /* Check for the matching sub-string */

      if (strncmp(vi->text + pos, vi->scratch, len) == 0)
        {
          vi_write(vi, g_fmtsrcbot, sizeof(g_fmtsrcbot));

          /* Found it... save the cursor position and
           * return success.
           */

          vi->curpos = pos;
          return true;
        }
    }

  return false;
}

/****************************************************************************
 * Name: vi_revfindstring
 *
 * Description:
 *   Find the string in the findstr buffer by searching for a matching
 *   sub-string in the text buffer, starting at the current cursor position.
 *   The search is performed backward through the file.
 *
 ****************************************************************************/

static bool vi_revfindstring(FAR struct vi_s *vi)
{
  off_t pos;
  int len;

  viinfo("findstr: \"%s\"\n", vi->findstr);

  /* The search string is in the find buffer */

  len = strlen(vi->findstr);
  if (!len)
    {
      return false;
    }

  /* Search from the current cursor position forward for a
   * matching sub-string.  Stop loo
   */

  vi_clearbottomline(vi);
  for (pos = vi->curpos;
       pos > 0; pos--)
    {
      /* Check for the matching sub-string */

      if (strncmp(vi->text + pos, vi->scratch, len) == 0)
        {
          /* Found it... save the cursor position and
           * return success.
           */

          vi->curpos = pos;
          return true;
        }
    }

  /* If we get here, then the search string was not found anywhere before the
   * current cursor position.  Start from end and search to curpos.
   */

  for (pos = vi->textsize - len; pos > vi->curpos; pos--)
    {
      /* Check for the matching sub-string */

      if (strncmp(vi->text + pos, vi->scratch, len) == 0)
        {
          vi_write(vi, g_fmtsrctop, sizeof(g_fmtsrctop));

          /* Found it... save the cursor position and
           * return success.
           */

          vi->curpos = pos;
          return true;
        }
    }

  return false;
}

/****************************************************************************
 * Name:  vi_parsefind
 *
 * Description:
 *   Find the string collected in the scratch buffer.
 *
 ****************************************************************************/

static void vi_parsefind(FAR struct vi_s *vi, bool revfind)
{
  /* Make certain that the scratch buffer contents are NUL terminated */

  vi->scratch[vi->cmdlen] = '\0';

  /* Is there anything in the scratch buffer? If not, then we will use the
   * string from the previous find operation.
   */

  viinfo("scratch: \"%s\"\n", vi->scratch);

  if (vi->cmdlen > 0)
    {
      /* Copy the new search string from the scratch to the find buffer */

      strncpy(vi->findstr, vi->scratch, MAX_STRING - 1);

      /* Make sure that the (possibly truncated) search string is NUL
       * terminated
       */

      vi->findstr[MAX_STRING - 1] = '\0';
    }

  /* Then attempt to find the string */

  vi->revfind = revfind;
  if (revfind)
    {
      vi_revfindstring(vi);
    }
  else
    {
      vi_findstring(vi);
    }

  /* Exit the sub-mode and revert to command mode */

  vi_exitsubmode(vi, MODE_COMMAND);
}

/****************************************************************************
 * Name: vi_find_submode
 *
 * Description:
 *   Find command sub-mode of the command mode processing
 *
 ****************************************************************************/

static void vi_find_submode(FAR struct vi_s *vi, bool revfind)
{
  int ch;

  viinfo("Enter find sub-mode\n");

  /* Loop while we are in find mode */

  while (vi->mode == SUBMODE_FIND || vi->mode == SUBMODE_REVFIND)
    {
      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_FINDMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              vi_cmdch(vi, vi_getch(vi));
            }
            break;

          case ASCII_BS: /* Delete the character before the cursor */
            {
              if (vi->cmdlen == 0)
                {
                  vi_exitsubmode(vi, MODE_COMMAND);

                  /* Ensure bottom line is cleared */

                  vi_clearbottomline(vi);
                }
              else
                {
                  vi_cmdbackspace(vi);
                }
            }
            break;

          case ASCII_ESC: /* Escape exits find mode */
            {
              vi_exitsubmode(vi, MODE_COMMAND);
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              vi_parsefind(vi, revfind);
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              vi_parsefind(vi, revfind);
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              vi_parsefind(vi, revfind);
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters */

              if (isprint(ch))
                {
                  /* Insert the filtered character into the scratch buffer */

                  vi_cmdch(vi, ch);
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }
}

/****************************************************************************
 * Replace Text Sub-Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_replacech
 *
 * Description:
 *   Replace the character at the current position.  If the current position
 *   is the end of line, then insert the character.
 *
 ****************************************************************************/

static void vi_replacech(FAR struct vi_s *vi, char ch)
{
  viinfo("curpos=%ld ch=%c[%02x]\n", vi->curpos, isprint(ch) ? ch : '.', ch);

  /* Is there a newline at the current cursor position? */

  if (vi->text[vi->curpos] == '\n')
    {
      /* Yes, then insert the new character before the newline */

      vi_insertch(vi, ch);
      vi->drawtoeos = true;
    }
  else
    {
      /* No, just replace the character and increment the cursor position */

      vi->text[vi->curpos++] = ch;
      vi->redrawline = true;
    }
}

/****************************************************************************
 * Name: vi_replacech_submode
 *
 * Description:
 *   Replace character command sub-mode of the command mode processing
 *
 ****************************************************************************/

static void vi_replacech_submode(FAR struct vi_s *vi)
{
  off_t end;
  long nchars;
  bool found = false;
  int ch = 0;

  /* Get the number of characters to replace */

  nchars = (vi->value > 0 ? vi->value : 1);

  viinfo("Enter replaces character(s) sub-mode: nchars=%d\n", nchars);

  /* Are there that many characters left on the line to be replaced? */

  end = vi_lineend(vi, vi->curpos) + 1;
  if (vi->curpos + nchars > end)
    {
      vi_error(vi, g_fmtnotvalid);
      vi_setmode(vi, MODE_COMMAND, 0);
    }

  /* Loop until we get the replacement character */

  while (vi->mode == SUBMODE_REPLACECH && !found)
    {
      /* Get the next character from the input */

      ch = vi_getch(vi);

      /* Handle the newly received character */

      vi->updatereqcol = true;
      switch (ch)
        {
          case KEY_FINDMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              ch    = vi_getch(vi);
              found = true;
            }
            break;

          case ASCII_ESC: /* Escape exits replace mode */
            {
              vi_setmode(vi, MODE_COMMAND, 0);
              if (vi->curpos > 0)
                {
                  --vi->curpos;
                }
            }
            break;

          /* What do we do with carriage returns? line feeds? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              ch = '\n';
              found = true;
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              found = true;
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              ch = '\n';
              found = true;
            }
            break;
#endif

          default:
            {
              /* Ignore all but printable characters and tab */

              if (isprint(ch)  || ch == '\t')
                {
                  found = true;
                }
              else
                {
                  VI_BEL(vi);
                }
            }

            break;
        }
    }

  /* Now replace with the character nchar times */

  for (; nchars > 0; nchars--)
    {
      vi_replacech(vi, ch);
      vi->redrawline = true;
    }

  /* Revert to command mode */

  vi_setmode(vi, MODE_COMMAND, 0);
}

/****************************************************************************
 * Name: vi_findinline_mode
 *
 * Description:
 *   Find character in current line.
 *
 ****************************************************************************/

static void vi_findinline_mode(FAR struct vi_s *vi)
{
  int count;
  off_t pos;
  int ch = -1;

  /* Get the next character from the input */

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
  /* Test for active cmdrepeat */

  if (vi->cmdrepeat)
    {
      /* Read next command from command buffer */

      ch = vi->cmdbuf[vi->cmdindex++];
    }
  else
#endif
    {
      while (ch == -1)
        {
          ch = vi_getch(vi);
        }
    }

  /* Ignore all but printable characters and tab */

  if (!isprint(ch))
    {
      VI_BEL(vi);
      vi_setmode(vi, MODE_COMMAND, 0);
      return;
    }

  vi->updatereqcol = true;

  /* Now find the character */

  pos = vi->curpos + 1;
  count = vi->value > 0 ? vi->value : 1;

  while (count > 0 && pos < vi->textsize - 1 && vi->text[pos] != '\n')
    {
      /* Increment to next character */

      pos++;

      /* Test if this character matches */

      if (vi->text[pos] == ch)
        {
          count--;
        }
    }

  /* Test if found */

  if (count == 0)
    {
      if (vi->tfind)
        {
          pos--;
        }

      /* Test if yank or del armed */

      if (vi->yankarm || vi->delarm || vi->chgarm)
        {
          /* Yank the text and possibly delete */

          vi_yanktext(vi, vi->curpos, pos, 1, vi->delarm || vi->chgarm);
          if (vi->delarm || vi->chgarm)
            {
              /* Redraw line if text deleted */

              vi->redrawline = true;
            }

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
          /* Setup command repeat */

          if (vi->delarm || vi->chgarm)
            {
              vi_saverepeat(vi, vi->delarm ? 'd' : 'c');
              vi_appendrepeat(vi, vi->tfind ? 't' : 'f');
              vi_appendrepeat(vi, ch);
            }
#endif

          /* If change text is armed, then enter insert mode */

          if (vi->chgarm)
            {
              vi->chgarm = false;
              vi_setmode(vi, MODE_INSERT, 0);
              return;
            }

          vi->delarm = false;
          vi->yankarm = false;
        }
      else
        {
          /* Simply move to the specified location */

          vi->curpos = pos;
        }
    }

  /* Revert to command mode */

  vi_setmode(vi, MODE_COMMAND, 0);
}

/****************************************************************************
 * Insert and Replace Mode Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_insertch
 *
 * Description:
 *   Insert one character into the text buffer
 *
 ****************************************************************************/

static void vi_insertch(FAR struct vi_s *vi, char ch)
{
  viinfo("curpos=%ld ch=%c[%02x]\n", vi->curpos, isprint(ch) ? ch : '.', ch);

  /* Make space in the buffer for the new character */

  if (vi_extendtext(vi, vi->curpos, 1))
    {
      /* Add the new character to the buffer */

      vi->text[vi->curpos++] = ch;
    }
}

/****************************************************************************
 * Name: vi_insert_mode
 *
 * Description:
 *   Insert mode loop
 *
 ****************************************************************************/

static void vi_insert_mode(FAR struct vi_s *vi)
{
  off_t start = vi->curpos;
  int ch;

  viinfo("Enter insert mode\n");

  /* Print insert message */

  vi_clearbottomline(vi);
  vi_write(vi, g_fmtinsert, sizeof(g_fmtinsert));
  vi_setcursor(vi, vi->cursor.row, vi->cursor.column);
  vi->redrawline = true;

  /* Loop while we are in insert mode */

  while (vi->mode == MODE_INSERT || vi->mode == MODE_REPLACE)
    {
      /* Make sure that the display reflects the current state */

      if (vi->redrawline || vi->drawtoeos || vi->fullredraw)
        {
          vi_showtext(vi);
        }

      /* Display the line and col number */

      vi_showlinecol(vi);
      vi_setcursor(vi, vi->cursor.row, vi->cursor.column);

      /* Get the next character from the input */

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
      /* Test for active cmdrepeat */

      if (vi->cmdrepeat)
        {
          /* Read next command from command buffer */

          ch = vi->cmdbuf[vi->cmdindex++];
        }
      else
#endif
        {
          ch = vi_getch(vi);

#ifdef CONFIG_SYSTEM_VI_INCLUDE_COMMAND_REPEAT
          /* Any arrow, pgup, pgdn, etc. key resets command repeat */

          if (ch == KEY_UP || ch == KEY_DOWN || ch == KEY_LEFT ||
              ch == KEY_RIGHT || ch == KEY_HOME || ch == KEY_END ||
              ch == KEY_PPAGE || ch == KEY_NPAGE)
            {
              vi->cmdcount = 1;
              vi->redrawline = true;
            }
          else
            {
              vi_appendrepeat(vi, ch);
            }
#endif
        }

      /* Test for printable character first since we will get mostly those,
       * and this will give better performance.
       */

      vi->updatereqcol = true;
      if (isprint(ch) || ch == '\t')
        {
          /* Insert the filtered character into the buffer */

          if (vi->mode == MODE_INSERT)
            {
              vi_insertch(vi, ch);
            }
          else
            {
              vi_replacech(vi, ch);
            }

          /* If we don't have to scroll the screen to display the
           * character, then do a simple putch, otherwise request
           * a line redraw.
           */

          if (vi->cursor.column + 1 < vi->display.column && ch != '\t' &&
              (vi->curpos + 1 == vi->textsize ||
               vi->text[vi->curpos + 1] == '\n'))
            {
              vi_putch(vi, ch);
            }
          else
            {
              vi->redrawline = true;
            }

          vi->cursor.column++;

          continue;
        }

      /* Any key press clears the error message */

      vi->error = false;
      vi->redrawline = true;

      /* Handle the newly received character */

      switch (ch)
        {
          case KEY_INSMODE_QUOTE: /* Quoted character follows */
            {
              /* Insert the next character unconditionally */

              vi_insertch(vi, vi_getch(vi));
            }
            break;

          case ASCII_DEL:
            {
              if (vi->curpos < vi->textsize)
                {
                  if (vi->text[vi->curpos] == '\n')
                    {
                      vi->drawtoeos = true;
                    }

                  vi_shrinktext(vi, vi->curpos, 1);
                }
            }
            break;

          case ASCII_BS:
            {
              /* Backspace changes based on mode */

              if (vi->mode == MODE_INSERT)
                {
                  /* In insert mode, we remove characters */

                  if (vi->curpos > 0)
                    {
                      if (vi->text[vi->curpos - 1] == '\n')
                        {
                          vi->drawtoeos = true;
                        }

                      vi_shrinktext(vi, vi->curpos - 1, 1);
                    }
                }
              else
                {
                  /* In replace mode, we simply move the cursor left */

                  if (vi->curpos > start)
                    {
                      vi->curpos = vi_cursorleft(vi, vi->curpos, 1);
                    }
                }
            }
            break;

          case ASCII_ESC: /* Escape exits insert mode */
            {
              vi_setmode(vi, MODE_COMMAND, 0);
              vi->updatereqcol = true;

              /* Move cursor 1 space to the left when exiting insert mode */

              if (vi->curpos > 0 && vi->text[vi->curpos - 1] != '\n')
                {
                  --vi->curpos;
                }
            }
            break;

          /* What do we do with carriage returns? */

#if defined(CONFIG_EOL_IS_CR)
          case '\r': /* CR terminates line */
            {
              if (vi->mode == MODE_INSERT)
                {
                  vi_insertch(vi, '\n');
                }
              else
                {
                  vi_replacech(vi, '\n');
                }

              vi->drawtoeos = true;
            }
            break;

#elif defined(CONFIG_EOL_IS_BOTH_CRLF)
         case '\r': /* Wait for the LF */
            break;
#endif

#if defined(CONFIG_EOL_IS_LF) || defined(CONFIG_EOL_IS_BOTH_CRLF)
          case '\n': /* LF terminates line */
            {
              if (vi->mode == MODE_INSERT)
                {
                  vi_insertch(vi, '\n');
                }
              else
                {
                  vi_replacech(vi, '\n');
                }

              vi->drawtoeos = true;
            }
            break;
#endif

#ifdef CONFIG_EOL_IS_EITHER_CRLF
          case '\r': /* Either CR or LF terminates line */
          case '\n':
            {
              if (vi->mode == MODE_INSERT)
                {
                  vi_insertch(vi, '\n');
                }
              else
                {
                  vi_replacech(vi, '\n');
                }

              vi_putch(vi, ' ');
              vi_clrtoeol(vi);
              vi->drawtoeos = true;
            }
            break;
#endif

          case KEY_UP:         /* Move the cursor up one line */
            {
              vi->updatereqcol = false;
              vi_cusorup(vi, 1);
            }
            break;

          case KEY_DOWN:         /* Move the cursor down one line */
            {
              vi->updatereqcol = false;
              vi_cursordown(vi, 1);
            }
            break;

          case KEY_LEFT:         /* Move the cursor left N characters */
            {
              vi->curpos = vi_cursorleft(vi, vi->curpos, 1);
            }
            break;

          case KEY_RIGHT:         /* Move the cursor right one character */
            {
              vi->curpos = vi_cursorright(vi, vi->curpos, 1);
              if (vi->curpos >= vi->textsize)
                {
                  vi->curpos = vi->textsize;
                }
            }
            break;

          case KEY_HOME:
            {
              vi->curpos = vi_linebegin(vi, vi->curpos);
            }
            break;

          case KEY_END:
            {
              vi->curpos = vi_lineend(vi, vi->curpos);
            }
            break;

          case KEY_PPAGE:
            {
              vi->updatereqcol = false;
              vi_cusorup(vi, vi->display.row);
            }
            break;

          case KEY_NPAGE:
            {
              vi->updatereqcol = false;
              vi_cursordown(vi, vi->display.row);
            }
            break;

          default:
            {
              /* Don't print BEL if char is -1 from termcurses */

              if (ch == -1)
                {
                  continue;
                }
              else
                {
                  VI_BEL(vi);
                }
            }
            break;
        }
    }

  vi_clearbottomline(vi);
}

/****************************************************************************
 * Command line processing
 ****************************************************************************/

/****************************************************************************
 * Name: vi_showusage
 *
 * Description:
 *   Show command line arguments and exit.
 *
 ****************************************************************************/

static void vi_release(FAR struct vi_s *vi)
{
  if (vi)
    {
      if (vi->text)
        {
          free(vi->text);
        }

      if (vi->yank)
        {
          free(vi->yank);
        }

      if (vi->tcurs)
        {
          termcurses_deinitterm(vi->tcurs);
        }

      free(vi);
    }
}

/****************************************************************************
 * Name: vi_showusage
 *
 * Description:
 *   Show command line arguments and exit.
 *
 ****************************************************************************/

static void vi_showusage(FAR struct vi_s *vi, FAR const char *progname,
                         int exitcode)
{
  fprintf(stderr, "\nUSAGE:\t%s [-c <columns] [-r <rows>] [<filename>]\n",
          progname);
  fprintf(stderr, "\nUSAGE:\t%s -h\n\n",
          progname);
  fprintf(stderr, "Where:\n");
  fprintf(stderr, "\t<filename>:\n");
  fprintf(stderr, "\t\tOptional name of the file to open\n");
  fprintf(stderr, "\t-c <columns>:\n");
  fprintf(stderr,
          "\t\tOptional width of the display in columns.  Default: %d\n",
          CONFIG_SYSTEM_VI_COLS);
  fprintf(stderr, "\t-r <rows>:\n");
  fprintf(stderr,
          "\t\tOptional height of the display in rows.  Default: %d\n",
          CONFIG_SYSTEM_VI_ROWS);
  fprintf(stderr, "\t-h:\n");
  fprintf(stderr, "\t\tShows this message and exits.\n");

  /* Release all allocated resources and exit */

  vi_release(vi);
  exit(exitcode);
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: vi_main
 *
 * Description:
 *   The main entry point into vi.
 *
 ****************************************************************************/

int main(int argc, FAR char *argv[])
{
  FAR struct vi_s *vi;
  int option;
  int ret;

  /* Allocate a vi state structure */

  vi = (FAR struct vi_s *)zalloc(sizeof(struct vi_s));
  if (vi == NULL)
    {
      vi_error(vi, g_fmtallocfail);
      return EXIT_FAILURE;
    }

  /* Initialize non-zero elements of the vi state structure */

  vi->display.row    = CONFIG_SYSTEM_VI_ROWS;
  vi->display.column = CONFIG_SYSTEM_VI_COLS;

  /* Parse command line arguments */

  while ((option = getopt(argc, argv, ":c:r:h")) != ERROR)
    {
      switch (option)
        {
          case 'c': /* Display width in columns */
            {
              unsigned long value = strtoul(optarg, NULL, 10);
              if (value <= UINT16_MAX)
                {
                  vi->display.column = (uint16_t)value;
                }
              else
                {
                  fprintf(stderr, "ERROR: Column value out of range: %lu\n",
                          value);
                  vi_showusage(vi, argv[0], EXIT_FAILURE);
                }
            }
            break;

          case 'r': /* Display width in columns */
            {
              unsigned long value = strtoul(optarg, NULL, 10);
              if (value <= UINT16_MAX)
                {
                  vi->display.row = (uint16_t)value;
                }
              else
                {
                  fprintf(stderr, "ERROR: Row value out of range: %lu\n",
                          value);
                  vi_showusage(vi, argv[0], EXIT_FAILURE);
                }
            }
            break;

          case 'h':
            {
              vi_showusage(vi, argv[0], EXIT_SUCCESS);
            }
            break;

          case ':':
            {
              fprintf(stderr, "ERROR: Missing parameter argument\n");
              vi_showusage(vi, argv[0], EXIT_FAILURE);
            }
            break;

          case '?':
          default:
            {
              fprintf(stderr, "ERROR: Unrecognized parameter\n");
              vi_showusage(vi, argv[0], EXIT_FAILURE);
            }
            break;
        }
    }

  /* Initialize termcurses */

  ret = termcurses_initterm(NULL, 0, 1, &vi->tcurs);
  if (ret == OK)
    {
      struct winsize winsz;

      ret = termcurses_getwinsize(vi->tcurs, &winsz);
      if (ret == OK)
        {
          vi->display.row    = winsz.ws_row;
          vi->display.column = winsz.ws_col;
        }
    }

  /* There maybe one additional argument on the command line: The filename */

  if (optind < argc)
    {
      /* Copy the file name into the file name buffer */

      if (argv[optind][0] == '/')
        {
          strncpy(vi->filename, argv[optind], MAX_STRING - 1);
        }
      else
        {
          /* Make file relative to current working directory */

          getcwd(vi->filename, MAX_STRING - 1);
          strncat(vi->filename, "/", MAX_STRING - 1);
          strncat(vi->filename, argv[optind], MAX_STRING - 1);
        }

      /* Make sure the (possibly truncated) file name is NUL terminated */

      vi->filename[MAX_STRING - 1] = '\0';

      /* Load the file into memory */

      vi_insertfile(vi, 0, vi->filename);
      vi->modified = false;

      /* Skip over the filename argument.  There should nothing after this */

      optind++;
    }

  /* If no file loaded, create an empty buffer for editing */

  if (vi->text == NULL)
    {
      vi_extendtext(vi, 0, TEXT_GULP_SIZE);
      vi->textsize = 0;
      vi->modified = 0;
    }

  if (optind != argc)
    {
      fprintf(stderr, "ERROR: Too many arguments\n");
      vi_showusage(vi, argv[0], EXIT_FAILURE);
    }

  /* The editor loop */

  vi->fullredraw = true;
  for (; ; )
    {
      /* We loop, processing each mode change */

      viinfo("mode=%d\n", vi->mode);

      switch (vi->mode)
        {
          default:
          case MODE_COMMAND:      /* Command mode */
            vi_cmd_mode(vi);
            break;

          case SUBMODE_COLON:     /* Colon data entry in command mode */
            vi_cmd_submode(vi);
            break;

          case SUBMODE_REVFIND:
          case SUBMODE_FIND:      /* Find data entry in command mode */
            vi_find_submode(vi, vi->mode == SUBMODE_REVFIND);
            break;

          case SUBMODE_REPLACECH: /* Replace characters in command mode */
            vi_replacech_submode(vi);
            break;

          case MODE_INSERT:       /* Insert mode */
          case MODE_REPLACE:      /* Replace character(s) under cursor until ESC */
            vi_insert_mode(vi);
            break;

          case MODE_FINDINLINE:       /* Insert mode */
            vi_findinline_mode(vi);
            break;
        }
    }

  /* We won't get here */
}
