2617a5e4c6
When the 'cc' command is invoked with autoindent enabled it should use the indent of the first line being changed. The size of the indent has to be established before char_insert() is called as the lines being changed are deleted. Introduce a new global variable, newindent, to handle this. The indentcol global is now effectively a static variable in char_insert(). function old new delta do_cmd 4247 4308 +61 vi_main 416 422 +6 char_insert 891 875 -16 ------------------------------------------------------------------------------ (add/remove: 0/0 grow/shrink: 2/1 up/down: 67/-16) Total: 51 bytes Signed-off-by: Ron Yorston <rmy@pobox.com> Signed-off-by: Denys Vlasenko <vda.linux@googlemail.com>
5047 lines
130 KiB
C
5047 lines
130 KiB
C
/* vi: set sw=4 ts=4: */
|
|
/*
|
|
* tiny vi.c: A small 'vi' clone
|
|
* Copyright (C) 2000, 2001 Sterling Huxley <sterling@europa.com>
|
|
*
|
|
* Licensed under GPLv2 or later, see file LICENSE in this source tree.
|
|
*/
|
|
//
|
|
//Things To Do:
|
|
// ./.exrc
|
|
// add magic to search /foo.*bar
|
|
// add :help command
|
|
// :map macros
|
|
// if mark[] values were line numbers rather than pointers
|
|
// it would be easier to change the mark when add/delete lines
|
|
// More intelligence in refresh()
|
|
// ":r !cmd" and "!cmd" to filter text through an external command
|
|
// An "ex" line oriented mode- maybe using "cmdedit"
|
|
|
|
//config:config VI
|
|
//config: bool "vi (23 kb)"
|
|
//config: default y
|
|
//config: help
|
|
//config: 'vi' is a text editor. More specifically, it is the One True
|
|
//config: text editor <grin>. It does, however, have a rather steep
|
|
//config: learning curve. If you are not already comfortable with 'vi'
|
|
//config: you may wish to use something else.
|
|
//config:
|
|
//config:config FEATURE_VI_MAX_LEN
|
|
//config: int "Maximum screen width"
|
|
//config: range 256 16384
|
|
//config: default 4096
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Contrary to what you may think, this is not eating much.
|
|
//config: Make it smaller than 4k only if you are very limited on memory.
|
|
//config:
|
|
//config:config FEATURE_VI_8BIT
|
|
//config: bool "Allow to display 8-bit chars (otherwise shows dots)"
|
|
//config: default n
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: If your terminal can display characters with high bit set,
|
|
//config: you may want to enable this. Note: vi is not Unicode-capable.
|
|
//config: If your terminal combines several 8-bit bytes into one character
|
|
//config: (as in Unicode mode), this will not work properly.
|
|
//config:
|
|
//config:config FEATURE_VI_COLON
|
|
//config: bool "Enable \":\" colon commands (no \"ex\" mode)"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Enable a limited set of colon commands. This does not
|
|
//config: provide an "ex" mode.
|
|
//config:
|
|
//config:config FEATURE_VI_COLON_EXPAND
|
|
//config: bool "Expand \"%\" and \"#\" in colon commands"
|
|
//config: default y
|
|
//config: depends on FEATURE_VI_COLON
|
|
//config: help
|
|
//config: Expand the special characters \"%\" (current filename)
|
|
//config: and \"#\" (alternate filename) in colon commands.
|
|
//config:
|
|
//config:config FEATURE_VI_YANKMARK
|
|
//config: bool "Enable yank/put commands and mark cmds"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: This enables you to use yank and put, as well as mark.
|
|
//config:
|
|
//config:config FEATURE_VI_SEARCH
|
|
//config: bool "Enable search and replace cmds"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Select this if you wish to be able to do search and replace.
|
|
//config:
|
|
//config:config FEATURE_VI_REGEX_SEARCH
|
|
//config: bool "Enable regex in search and replace"
|
|
//config: default n # Uses GNU regex, which may be unavailable. FIXME
|
|
//config: depends on FEATURE_VI_SEARCH
|
|
//config: help
|
|
//config: Use extended regex search.
|
|
//config:
|
|
//config:config FEATURE_VI_USE_SIGNALS
|
|
//config: bool "Catch signals"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Selecting this option will make vi signal aware. This will support
|
|
//config: SIGWINCH to deal with Window Changes, catch ^Z and ^C and alarms.
|
|
//config:
|
|
//config:config FEATURE_VI_DOT_CMD
|
|
//config: bool "Remember previous cmd and \".\" cmd"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Make vi remember the last command and be able to repeat it.
|
|
//config:
|
|
//config:config FEATURE_VI_READONLY
|
|
//config: bool "Enable -R option and \"view\" mode"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Enable the read-only command line option, which allows the user to
|
|
//config: open a file in read-only mode.
|
|
//config:
|
|
//config:config FEATURE_VI_SETOPTS
|
|
//config: bool "Enable settable options, ai ic showmatch"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Enable the editor to set some (ai, ic, showmatch) options.
|
|
//config:
|
|
//config:config FEATURE_VI_SET
|
|
//config: bool "Support :set"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config:
|
|
//config:config FEATURE_VI_WIN_RESIZE
|
|
//config: bool "Handle window resize"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Behave nicely with terminals that get resized.
|
|
//config:
|
|
//config:config FEATURE_VI_ASK_TERMINAL
|
|
//config: bool "Use 'tell me cursor position' ESC sequence to measure window"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: If terminal size can't be retrieved and $LINES/$COLUMNS are not set,
|
|
//config: this option makes vi perform a last-ditch effort to find it:
|
|
//config: position cursor to 999,999 and ask terminal to report real
|
|
//config: cursor position using "ESC [ 6 n" escape sequence, then read stdin.
|
|
//config: This is not clean but helps a lot on serial lines and such.
|
|
//config:
|
|
//config:config FEATURE_VI_UNDO
|
|
//config: bool "Support undo command \"u\""
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Support the 'u' command to undo insertion, deletion, and replacement
|
|
//config: of text.
|
|
//config:
|
|
//config:config FEATURE_VI_UNDO_QUEUE
|
|
//config: bool "Enable undo operation queuing"
|
|
//config: default y
|
|
//config: depends on FEATURE_VI_UNDO
|
|
//config: help
|
|
//config: The vi undo functions can use an intermediate queue to greatly lower
|
|
//config: malloc() calls and overhead. When the maximum size of this queue is
|
|
//config: reached, the contents of the queue are committed to the undo stack.
|
|
//config: This increases the size of the undo code and allows some undo
|
|
//config: operations (especially un-typing/backspacing) to be far more useful.
|
|
//config:
|
|
//config:config FEATURE_VI_UNDO_QUEUE_MAX
|
|
//config: int "Maximum undo character queue size"
|
|
//config: default 256
|
|
//config: range 32 65536
|
|
//config: depends on FEATURE_VI_UNDO_QUEUE
|
|
//config: help
|
|
//config: This option sets the number of bytes used at runtime for the queue.
|
|
//config: Smaller values will create more undo objects and reduce the amount
|
|
//config: of typed or backspaced characters that are grouped into one undo
|
|
//config: operation; larger values increase the potential size of each undo
|
|
//config: and will generally malloc() larger objects and less frequently.
|
|
//config: Unless you want more (or less) frequent "undo points" while typing,
|
|
//config: you should probably leave this unchanged.
|
|
//config:
|
|
//config:config FEATURE_VI_VERBOSE_STATUS
|
|
//config: bool "Enable verbose status reporting"
|
|
//config: default y
|
|
//config: depends on VI
|
|
//config: help
|
|
//config: Enable more verbose reporting of the results of yank, change,
|
|
//config: delete, undo and substitution commands.
|
|
|
|
//applet:IF_VI(APPLET(vi, BB_DIR_BIN, BB_SUID_DROP))
|
|
|
|
//kbuild:lib-$(CONFIG_VI) += vi.o
|
|
|
|
//usage:#define vi_trivial_usage
|
|
//usage: IF_FEATURE_VI_COLON("[-c CMD] ")IF_FEATURE_VI_READONLY("[-R] ")"[-H] [FILE]..."
|
|
//usage:#define vi_full_usage "\n\n"
|
|
//usage: "Edit FILE\n"
|
|
//usage: IF_FEATURE_VI_COLON(
|
|
//usage: "\n -c CMD Initial command to run ($EXINIT and ~/.exrc also available)"
|
|
//usage: )
|
|
//usage: IF_FEATURE_VI_READONLY(
|
|
//usage: "\n -R Read-only"
|
|
//usage: )
|
|
//usage: "\n -H List available features"
|
|
// note: non-standard, "vim -H" is Hebrew mode (bidi support)
|
|
|
|
#include "libbb.h"
|
|
// Should be after libbb.h: on some systems regex.h needs sys/types.h:
|
|
#if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
# include <regex.h>
|
|
#endif
|
|
|
|
// the CRASHME code is unmaintained, and doesn't currently build
|
|
#define ENABLE_FEATURE_VI_CRASHME 0
|
|
#define IF_FEATURE_VI_CRASHME(...)
|
|
|
|
|
|
#if ENABLE_LOCALE_SUPPORT
|
|
|
|
#if ENABLE_FEATURE_VI_8BIT
|
|
//FIXME: this does not work properly for Unicode anyway
|
|
# define Isprint(c) (isprint)(c)
|
|
#else
|
|
# define Isprint(c) isprint_asciionly(c)
|
|
#endif
|
|
|
|
#else
|
|
|
|
// 0x9b is Meta-ESC
|
|
#if ENABLE_FEATURE_VI_8BIT
|
|
# define Isprint(c) ((unsigned char)(c) >= ' ' && (c) != 0x7f && (unsigned char)(c) != 0x9b)
|
|
#else
|
|
# define Isprint(c) ((unsigned char)(c) >= ' ' && (unsigned char)(c) < 0x7f)
|
|
#endif
|
|
|
|
#endif
|
|
|
|
#define isbackspace(c) ((c) == term_orig.c_cc[VERASE] || (c) == 8 || (c) == 127)
|
|
|
|
enum {
|
|
MAX_TABSTOP = 32, // sanity limit
|
|
// User input len. Need not be extra big.
|
|
// Lines in file being edited *can* be bigger than this.
|
|
MAX_INPUT_LEN = 128,
|
|
// Sanity limits. We have only one buffer of this size.
|
|
MAX_SCR_COLS = CONFIG_FEATURE_VI_MAX_LEN,
|
|
MAX_SCR_ROWS = CONFIG_FEATURE_VI_MAX_LEN,
|
|
};
|
|
|
|
// VT102 ESC sequences.
|
|
// See "Xterm Control Sequences"
|
|
// http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
|
#define ESC "\033"
|
|
// Inverse/Normal text
|
|
#define ESC_BOLD_TEXT ESC"[7m"
|
|
#define ESC_NORM_TEXT ESC"[m"
|
|
// Bell
|
|
#define ESC_BELL "\007"
|
|
// Clear-to-end-of-line
|
|
#define ESC_CLEAR2EOL ESC"[K"
|
|
// Clear-to-end-of-screen.
|
|
// (We use default param here.
|
|
// Full sequence is "ESC [ <num> J",
|
|
// <num> is 0/1/2 = "erase below/above/all".)
|
|
#define ESC_CLEAR2EOS ESC"[J"
|
|
// Cursor to given coordinate (1,1: top left)
|
|
#define ESC_SET_CURSOR_POS ESC"[%u;%uH"
|
|
#define ESC_SET_CURSOR_TOPLEFT ESC"[H"
|
|
//UNUSED
|
|
//// Cursor up and down
|
|
//#define ESC_CURSOR_UP ESC"[A"
|
|
//#define ESC_CURSOR_DOWN "\n"
|
|
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
// cmds modifying text[]
|
|
static const char modifying_cmds[] ALIGN1 = "aAcCdDiIJoOpPrRs""xX<>~";
|
|
#endif
|
|
|
|
enum {
|
|
YANKONLY = FALSE,
|
|
YANKDEL = TRUE,
|
|
FORWARD = 1, // code depends on "1" for array index
|
|
BACK = -1, // code depends on "-1" for array index
|
|
LIMITED = 0, // char_search() only current line
|
|
FULL = 1, // char_search() to the end/beginning of entire text
|
|
PARTIAL = 0, // buffer contains partial line
|
|
WHOLE = 1, // buffer contains whole lines
|
|
MULTI = 2, // buffer may include newlines
|
|
|
|
S_BEFORE_WS = 1, // used in skip_thing() for moving "dot"
|
|
S_TO_WS = 2, // used in skip_thing() for moving "dot"
|
|
S_OVER_WS = 3, // used in skip_thing() for moving "dot"
|
|
S_END_PUNCT = 4, // used in skip_thing() for moving "dot"
|
|
S_END_ALNUM = 5, // used in skip_thing() for moving "dot"
|
|
|
|
C_END = -1, // cursor is at end of line due to '$' command
|
|
};
|
|
|
|
|
|
// vi.c expects chars to be unsigned.
|
|
// busybox build system provides that, but it's better
|
|
// to audit and fix the source
|
|
|
|
struct globals {
|
|
// many references - keep near the top of globals
|
|
char *text, *end; // pointers to the user data in memory
|
|
char *dot; // where all the action takes place
|
|
int text_size; // size of the allocated buffer
|
|
|
|
// the rest
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
smallint vi_setops; // set by setops()
|
|
#define VI_AUTOINDENT (1 << 0)
|
|
#define VI_EXPANDTAB (1 << 1)
|
|
#define VI_ERR_METHOD (1 << 2)
|
|
#define VI_IGNORECASE (1 << 3)
|
|
#define VI_SHOWMATCH (1 << 4)
|
|
#define VI_TABSTOP (1 << 5)
|
|
#define autoindent (vi_setops & VI_AUTOINDENT)
|
|
#define expandtab (vi_setops & VI_EXPANDTAB )
|
|
#define err_method (vi_setops & VI_ERR_METHOD) // indicate error with beep or flash
|
|
#define ignorecase (vi_setops & VI_IGNORECASE)
|
|
#define showmatch (vi_setops & VI_SHOWMATCH )
|
|
// order of constants and strings must match
|
|
#define OPTS_STR \
|
|
"ai\0""autoindent\0" \
|
|
"et\0""expandtab\0" \
|
|
"fl\0""flash\0" \
|
|
"ic\0""ignorecase\0" \
|
|
"sm\0""showmatch\0" \
|
|
"ts\0""tabstop\0"
|
|
#else
|
|
#define autoindent (0)
|
|
#define expandtab (0)
|
|
#define err_method (0)
|
|
#define ignorecase (0)
|
|
#endif
|
|
|
|
#if ENABLE_FEATURE_VI_READONLY
|
|
smallint readonly_mode;
|
|
#define SET_READONLY_FILE(flags) ((flags) |= 0x01)
|
|
#define SET_READONLY_MODE(flags) ((flags) |= 0x02)
|
|
#define UNSET_READONLY_FILE(flags) ((flags) &= 0xfe)
|
|
#else
|
|
#define SET_READONLY_FILE(flags) ((void)0)
|
|
#define SET_READONLY_MODE(flags) ((void)0)
|
|
#define UNSET_READONLY_FILE(flags) ((void)0)
|
|
#endif
|
|
|
|
smallint editing; // >0 while we are editing a file
|
|
// [code audit says "can be 0, 1 or 2 only"]
|
|
smallint cmd_mode; // 0=command 1=insert 2=replace
|
|
int modified_count; // buffer contents changed if !0
|
|
int last_modified_count; // = -1;
|
|
int cmdline_filecnt; // how many file names on cmd line
|
|
int cmdcnt; // repetition count
|
|
char *rstart; // start of text in Replace mode
|
|
unsigned rows, columns; // the terminal screen is this size
|
|
#if ENABLE_FEATURE_VI_ASK_TERMINAL
|
|
int get_rowcol_error;
|
|
#endif
|
|
int crow, ccol; // cursor is on Crow x Ccol
|
|
int offset; // chars scrolled off the screen to the left
|
|
int have_status_msg; // is default edit status needed?
|
|
// [don't make smallint!]
|
|
int last_status_cksum; // hash of current status line
|
|
char *current_filename;
|
|
#if ENABLE_FEATURE_VI_COLON_EXPAND
|
|
char *alt_filename;
|
|
#endif
|
|
char *screenbegin; // index into text[], of top line on the screen
|
|
char *screen; // pointer to the virtual screen buffer
|
|
int screensize; // and its size
|
|
int tabstop;
|
|
int last_search_char; // last char searched for (int because of Unicode)
|
|
smallint last_search_cmd; // command used to invoke last char search
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
char last_input_char; // last char read from user
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
char undo_queue_state; // One of UNDO_INS, UNDO_DEL, UNDO_EMPTY
|
|
#endif
|
|
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
smallint adding2q; // are we currently adding user input to q
|
|
int lmc_len; // length of last_modifying_cmd
|
|
char *ioq, *ioq_start; // pointer to string for get_one_char to "read"
|
|
int dotcnt; // number of times to repeat '.' command
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_SEARCH
|
|
char *last_search_pattern; // last pattern from a '/' or '?' search
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
int char_insert__indentcol; // column of recent autoindent or 0
|
|
int newindent; // autoindent value for 'O'/'cc' commands
|
|
// or -1 to use indent from previous line
|
|
#endif
|
|
smallint cmd_error;
|
|
|
|
// former statics
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
char *edit_file__cur_line;
|
|
#endif
|
|
int refresh__old_offset;
|
|
int format_edit_status__tot;
|
|
|
|
// a few references only
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
smalluint YDreg;//,Ureg;// default delete register and orig line for "U"
|
|
#define Ureg 27
|
|
char *reg[28]; // named register a-z, "D", and "U" 0-25,26,27
|
|
char regtype[28]; // buffer type: WHOLE, MULTI or PARTIAL
|
|
char *mark[28]; // user marks points somewhere in text[]- a-z and previous context ''
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_USE_SIGNALS
|
|
sigjmp_buf restart; // int_handler() jumps to location remembered here
|
|
#endif
|
|
struct termios term_orig; // remember what the cooked mode was
|
|
int cindex; // saved character index for up/down motion
|
|
smallint keep_index; // retain saved character index
|
|
#if ENABLE_FEATURE_VI_COLON
|
|
llist_t *initial_cmds;
|
|
#endif
|
|
// Should be just enough to hold a key sequence,
|
|
// but CRASHME mode uses it as generated command buffer too
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
char readbuffer[128];
|
|
#else
|
|
char readbuffer[KEYCODE_BUFFER_SIZE];
|
|
#endif
|
|
#define STATUS_BUFFER_LEN 200
|
|
char status_buffer[STATUS_BUFFER_LEN]; // messages to the user
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
char last_modifying_cmd[MAX_INPUT_LEN]; // last modifying cmd for "."
|
|
#endif
|
|
char get_input_line__buf[MAX_INPUT_LEN]; // former static
|
|
|
|
char scr_out_buf[MAX_SCR_COLS + MAX_TABSTOP * 2];
|
|
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
// undo_push() operations
|
|
#define UNDO_INS 0
|
|
#define UNDO_DEL 1
|
|
#define UNDO_INS_CHAIN 2
|
|
#define UNDO_DEL_CHAIN 3
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
#define UNDO_INS_QUEUED 4
|
|
#define UNDO_DEL_QUEUED 5
|
|
# endif
|
|
|
|
// Pass-through flags for functions that can be undone
|
|
#define NO_UNDO 0
|
|
#define ALLOW_UNDO 1
|
|
#define ALLOW_UNDO_CHAIN 2
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
#define ALLOW_UNDO_QUEUED 3
|
|
# else
|
|
// If undo queuing disabled, don't invoke the missing queue logic
|
|
#define ALLOW_UNDO_QUEUED ALLOW_UNDO
|
|
# endif
|
|
|
|
struct undo_object {
|
|
struct undo_object *prev; // Linking back avoids list traversal (LIFO)
|
|
int start; // Offset where the data should be restored/deleted
|
|
int length; // total data size
|
|
uint8_t u_type; // 0=deleted, 1=inserted, 2=swapped
|
|
char undo_text[1]; // text that was deleted (if deletion)
|
|
} *undo_stack_tail;
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
#define UNDO_USE_SPOS 32
|
|
#define UNDO_EMPTY 64
|
|
char *undo_queue_spos; // Start position of queued operation
|
|
int undo_q;
|
|
char undo_queue[CONFIG_FEATURE_VI_UNDO_QUEUE_MAX];
|
|
# endif
|
|
#endif /* ENABLE_FEATURE_VI_UNDO */
|
|
};
|
|
#define G (*ptr_to_globals)
|
|
#define text (G.text )
|
|
#define text_size (G.text_size )
|
|
#define end (G.end )
|
|
#define dot (G.dot )
|
|
#define reg (G.reg )
|
|
|
|
#define vi_setops (G.vi_setops )
|
|
#define editing (G.editing )
|
|
#define cmd_mode (G.cmd_mode )
|
|
#define modified_count (G.modified_count )
|
|
#define last_modified_count (G.last_modified_count)
|
|
#define cmdline_filecnt (G.cmdline_filecnt )
|
|
#define cmdcnt (G.cmdcnt )
|
|
#define rstart (G.rstart )
|
|
#define rows (G.rows )
|
|
#define columns (G.columns )
|
|
#define crow (G.crow )
|
|
#define ccol (G.ccol )
|
|
#define offset (G.offset )
|
|
#define status_buffer (G.status_buffer )
|
|
#define have_status_msg (G.have_status_msg )
|
|
#define last_status_cksum (G.last_status_cksum )
|
|
#define current_filename (G.current_filename )
|
|
#define alt_filename (G.alt_filename )
|
|
#define screen (G.screen )
|
|
#define screensize (G.screensize )
|
|
#define screenbegin (G.screenbegin )
|
|
#define tabstop (G.tabstop )
|
|
#define last_search_char (G.last_search_char )
|
|
#define last_search_cmd (G.last_search_cmd )
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
#define last_input_char (G.last_input_char )
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_READONLY
|
|
#define readonly_mode (G.readonly_mode )
|
|
#else
|
|
#define readonly_mode 0
|
|
#endif
|
|
#define adding2q (G.adding2q )
|
|
#define lmc_len (G.lmc_len )
|
|
#define ioq (G.ioq )
|
|
#define ioq_start (G.ioq_start )
|
|
#define dotcnt (G.dotcnt )
|
|
#define last_search_pattern (G.last_search_pattern)
|
|
#define char_insert__indentcol (G.char_insert__indentcol)
|
|
#define newindent (G.newindent )
|
|
#define cmd_error (G.cmd_error )
|
|
|
|
#define edit_file__cur_line (G.edit_file__cur_line)
|
|
#define refresh__old_offset (G.refresh__old_offset)
|
|
#define format_edit_status__tot (G.format_edit_status__tot)
|
|
|
|
#define YDreg (G.YDreg )
|
|
//#define Ureg (G.Ureg )
|
|
#define regtype (G.regtype )
|
|
#define mark (G.mark )
|
|
#define restart (G.restart )
|
|
#define term_orig (G.term_orig )
|
|
#define cindex (G.cindex )
|
|
#define keep_index (G.keep_index )
|
|
#define initial_cmds (G.initial_cmds )
|
|
#define readbuffer (G.readbuffer )
|
|
#define scr_out_buf (G.scr_out_buf )
|
|
#define last_modifying_cmd (G.last_modifying_cmd )
|
|
#define get_input_line__buf (G.get_input_line__buf)
|
|
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
#define undo_stack_tail (G.undo_stack_tail )
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
#define undo_queue_state (G.undo_queue_state)
|
|
#define undo_q (G.undo_q )
|
|
#define undo_queue (G.undo_queue )
|
|
#define undo_queue_spos (G.undo_queue_spos )
|
|
# endif
|
|
#endif
|
|
|
|
#define INIT_G() do { \
|
|
SET_PTR_TO_GLOBALS(xzalloc(sizeof(G))); \
|
|
last_modified_count--; \
|
|
/* "" but has space for 2 chars: */ \
|
|
IF_FEATURE_VI_SEARCH(last_search_pattern = xzalloc(2);) \
|
|
tabstop = 8; \
|
|
IF_FEATURE_VI_SETOPTS(newindent--;) \
|
|
} while (0)
|
|
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
static int crashme = 0;
|
|
#endif
|
|
|
|
static void show_status_line(void); // put a message on the bottom line
|
|
static void status_line_bold(const char *, ...);
|
|
|
|
static void show_help(void)
|
|
{
|
|
puts("These features are available:"
|
|
#if ENABLE_FEATURE_VI_SEARCH
|
|
"\n\tPattern searches with / and ?"
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
"\n\tLast command repeat with ."
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
"\n\tLine marking with 'x"
|
|
"\n\tNamed buffers with \"x"
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_READONLY
|
|
//not implemented: "\n\tReadonly if vi is called as \"view\""
|
|
//redundant: usage text says this too: "\n\tReadonly with -R command line arg"
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_SET
|
|
"\n\tSome colon mode commands with :"
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
"\n\tSettable options with \":set\""
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_USE_SIGNALS
|
|
"\n\tSignal catching- ^C"
|
|
"\n\tJob suspend and resume with ^Z"
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_WIN_RESIZE
|
|
"\n\tAdapt to window re-sizes"
|
|
#endif
|
|
);
|
|
}
|
|
|
|
static void write1(const char *out)
|
|
{
|
|
fputs_stdout(out);
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_WIN_RESIZE
|
|
static int query_screen_dimensions(void)
|
|
{
|
|
int err = get_terminal_width_height(STDIN_FILENO, &columns, &rows);
|
|
if (rows > MAX_SCR_ROWS)
|
|
rows = MAX_SCR_ROWS;
|
|
if (columns > MAX_SCR_COLS)
|
|
columns = MAX_SCR_COLS;
|
|
return err;
|
|
}
|
|
#else
|
|
static ALWAYS_INLINE int query_screen_dimensions(void)
|
|
{
|
|
return 0;
|
|
}
|
|
#endif
|
|
|
|
// sleep for 'h' 1/100 seconds, return 1/0 if stdin is (ready for read)/(not ready)
|
|
static int mysleep(int hund)
|
|
{
|
|
struct pollfd pfd[1];
|
|
|
|
if (hund != 0)
|
|
fflush_all();
|
|
|
|
pfd[0].fd = STDIN_FILENO;
|
|
pfd[0].events = POLLIN;
|
|
return safe_poll(pfd, 1, hund*10) > 0;
|
|
}
|
|
|
|
//----- Set terminal attributes --------------------------------
|
|
static void rawmode(void)
|
|
{
|
|
// no TERMIOS_CLEAR_ISIG: leave ISIG on - allow signals
|
|
set_termios_to_raw(STDIN_FILENO, &term_orig, TERMIOS_RAW_CRNL);
|
|
}
|
|
|
|
static void cookmode(void)
|
|
{
|
|
fflush_all();
|
|
tcsetattr_stdin_TCSANOW(&term_orig);
|
|
}
|
|
|
|
//----- Terminal Drawing ---------------------------------------
|
|
// The terminal is made up of 'rows' line of 'columns' columns.
|
|
// classically this would be 24 x 80.
|
|
// screen coordinates
|
|
// 0,0 ... 0,79
|
|
// 1,0 ... 1,79
|
|
// . ... .
|
|
// . ... .
|
|
// 22,0 ... 22,79
|
|
// 23,0 ... 23,79 <- status line
|
|
|
|
//----- Move the cursor to row x col (count from 0, not 1) -------
|
|
static void place_cursor(int row, int col)
|
|
{
|
|
char cm1[sizeof(ESC_SET_CURSOR_POS) + sizeof(int)*3 * 2];
|
|
|
|
if (row < 0) row = 0;
|
|
if (row >= rows) row = rows - 1;
|
|
if (col < 0) col = 0;
|
|
if (col >= columns) col = columns - 1;
|
|
|
|
sprintf(cm1, ESC_SET_CURSOR_POS, row + 1, col + 1);
|
|
write1(cm1);
|
|
}
|
|
|
|
//----- Erase from cursor to end of line -----------------------
|
|
static void clear_to_eol(void)
|
|
{
|
|
write1(ESC_CLEAR2EOL);
|
|
}
|
|
|
|
static void go_bottom_and_clear_to_eol(void)
|
|
{
|
|
place_cursor(rows - 1, 0);
|
|
clear_to_eol();
|
|
}
|
|
|
|
//----- Start standout mode ------------------------------------
|
|
static void standout_start(void)
|
|
{
|
|
write1(ESC_BOLD_TEXT);
|
|
}
|
|
|
|
//----- End standout mode --------------------------------------
|
|
static void standout_end(void)
|
|
{
|
|
write1(ESC_NORM_TEXT);
|
|
}
|
|
|
|
//----- Text Movement Routines ---------------------------------
|
|
static char *begin_line(char *p) // return pointer to first char cur line
|
|
{
|
|
if (p > text) {
|
|
p = memrchr(text, '\n', p - text);
|
|
if (!p)
|
|
return text;
|
|
return p + 1;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static char *end_line(char *p) // return pointer to NL of cur line
|
|
{
|
|
if (p < end - 1) {
|
|
p = memchr(p, '\n', end - p - 1);
|
|
if (!p)
|
|
return end - 1;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
static char *dollar_line(char *p) // return pointer to just before NL line
|
|
{
|
|
p = end_line(p);
|
|
// Try to stay off of the Newline
|
|
if (*p == '\n' && (p - begin_line(p)) > 0)
|
|
p--;
|
|
return p;
|
|
}
|
|
|
|
static char *prev_line(char *p) // return pointer first char prev line
|
|
{
|
|
p = begin_line(p); // goto beginning of cur line
|
|
if (p > text && p[-1] == '\n')
|
|
p--; // step to prev line
|
|
p = begin_line(p); // goto beginning of prev line
|
|
return p;
|
|
}
|
|
|
|
static char *next_line(char *p) // return pointer first char next line
|
|
{
|
|
p = end_line(p);
|
|
if (p < end - 1 && *p == '\n')
|
|
p++; // step to next line
|
|
return p;
|
|
}
|
|
|
|
//----- Text Information Routines ------------------------------
|
|
static char *end_screen(void)
|
|
{
|
|
char *q;
|
|
int cnt;
|
|
|
|
// find new bottom line
|
|
q = screenbegin;
|
|
for (cnt = 0; cnt < rows - 2; cnt++)
|
|
q = next_line(q);
|
|
q = end_line(q);
|
|
return q;
|
|
}
|
|
|
|
// count line from start to stop
|
|
static int count_lines(char *start, char *stop)
|
|
{
|
|
char *q;
|
|
int cnt;
|
|
|
|
if (stop < start) { // start and stop are backwards- reverse them
|
|
q = start;
|
|
start = stop;
|
|
stop = q;
|
|
}
|
|
cnt = 0;
|
|
stop = end_line(stop);
|
|
while (start <= stop && start <= end - 1) {
|
|
start = end_line(start);
|
|
if (*start == '\n')
|
|
cnt++;
|
|
start++;
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
static char *find_line(int li) // find beginning of line #li
|
|
{
|
|
char *q;
|
|
|
|
for (q = text; li > 1; li--) {
|
|
q = next_line(q);
|
|
}
|
|
return q;
|
|
}
|
|
|
|
static int next_tabstop(int col)
|
|
{
|
|
return col + ((tabstop - 1) - (col % tabstop));
|
|
}
|
|
|
|
static int prev_tabstop(int col)
|
|
{
|
|
return col - ((col % tabstop) ?: tabstop);
|
|
}
|
|
|
|
static int next_column(char c, int co)
|
|
{
|
|
if (c == '\t')
|
|
co = next_tabstop(co);
|
|
else if ((unsigned char)c < ' ' || c == 0x7f)
|
|
co++; // display as ^X, use 2 columns
|
|
return co + 1;
|
|
}
|
|
|
|
static int get_column(char *p)
|
|
{
|
|
const char *r;
|
|
int co = 0;
|
|
|
|
for (r = begin_line(p); r < p; r++)
|
|
co = next_column(*r, co);
|
|
return co;
|
|
}
|
|
|
|
//----- Erase the Screen[] memory ------------------------------
|
|
static void screen_erase(void)
|
|
{
|
|
memset(screen, ' ', screensize); // clear new screen
|
|
}
|
|
|
|
static void new_screen(int ro, int co)
|
|
{
|
|
char *s;
|
|
|
|
free(screen);
|
|
screensize = ro * co + 8;
|
|
s = screen = xmalloc(screensize);
|
|
// initialize the new screen. assume this will be a empty file.
|
|
screen_erase();
|
|
// non-existent text[] lines start with a tilde (~).
|
|
//screen[(1 * co) + 0] = '~';
|
|
//screen[(2 * co) + 0] = '~';
|
|
//..
|
|
//screen[((ro-2) * co) + 0] = '~';
|
|
ro -= 2;
|
|
while (--ro >= 0) {
|
|
s += co;
|
|
*s = '~';
|
|
}
|
|
}
|
|
|
|
//----- Synchronize the cursor to Dot --------------------------
|
|
static void sync_cursor(char *d, int *row, int *col)
|
|
{
|
|
char *beg_cur; // begin and end of "d" line
|
|
char *tp;
|
|
int cnt, ro, co;
|
|
|
|
beg_cur = begin_line(d); // first char of cur line
|
|
|
|
if (beg_cur < screenbegin) {
|
|
// "d" is before top line on screen
|
|
// how many lines do we have to move
|
|
cnt = count_lines(beg_cur, screenbegin);
|
|
sc1:
|
|
screenbegin = beg_cur;
|
|
if (cnt > (rows - 1) / 2) {
|
|
// we moved too many lines. put "dot" in middle of screen
|
|
for (cnt = 0; cnt < (rows - 1) / 2; cnt++) {
|
|
screenbegin = prev_line(screenbegin);
|
|
}
|
|
}
|
|
} else {
|
|
char *end_scr; // begin and end of screen
|
|
end_scr = end_screen(); // last char of screen
|
|
if (beg_cur > end_scr) {
|
|
// "d" is after bottom line on screen
|
|
// how many lines do we have to move
|
|
cnt = count_lines(end_scr, beg_cur);
|
|
if (cnt > (rows - 1) / 2)
|
|
goto sc1; // too many lines
|
|
for (ro = 0; ro < cnt - 1; ro++) {
|
|
// move screen begin the same amount
|
|
screenbegin = next_line(screenbegin);
|
|
// now, move the end of screen
|
|
end_scr = next_line(end_scr);
|
|
end_scr = end_line(end_scr);
|
|
}
|
|
}
|
|
}
|
|
// "d" is on screen- find out which row
|
|
tp = screenbegin;
|
|
for (ro = 0; ro < rows - 1; ro++) { // drive "ro" to correct row
|
|
if (tp == beg_cur)
|
|
break;
|
|
tp = next_line(tp);
|
|
}
|
|
|
|
// find out what col "d" is on
|
|
co = 0;
|
|
do { // drive "co" to correct column
|
|
if (*tp == '\n') //vda || *tp == '\0')
|
|
break;
|
|
co = next_column(*tp, co) - 1;
|
|
// inserting text before a tab, don't include its position
|
|
if (cmd_mode && tp == d - 1 && *d == '\t') {
|
|
co++;
|
|
break;
|
|
}
|
|
} while (tp++ < d && ++co);
|
|
|
|
// "co" is the column where "dot" is.
|
|
// The screen has "columns" columns.
|
|
// The currently displayed columns are 0+offset -- columns+ofset
|
|
// |-------------------------------------------------------------|
|
|
// ^ ^ ^
|
|
// offset | |------- columns ----------------|
|
|
//
|
|
// If "co" is already in this range then we do not have to adjust offset
|
|
// but, we do have to subtract the "offset" bias from "co".
|
|
// If "co" is outside this range then we have to change "offset".
|
|
// If the first char of a line is a tab the cursor will try to stay
|
|
// in column 7, but we have to set offset to 0.
|
|
|
|
if (co < 0 + offset) {
|
|
offset = co;
|
|
}
|
|
if (co >= columns + offset) {
|
|
offset = co - columns + 1;
|
|
}
|
|
// if the first char of the line is a tab, and "dot" is sitting on it
|
|
// force offset to 0.
|
|
if (d == beg_cur && *d == '\t') {
|
|
offset = 0;
|
|
}
|
|
co -= offset;
|
|
|
|
*row = ro;
|
|
*col = co;
|
|
}
|
|
|
|
//----- Format a text[] line into a buffer ---------------------
|
|
static char* format_line(char *src /*, int li*/)
|
|
{
|
|
unsigned char c;
|
|
int co;
|
|
int ofs = offset;
|
|
char *dest = scr_out_buf; // [MAX_SCR_COLS + MAX_TABSTOP * 2]
|
|
|
|
c = '~'; // char in col 0 in non-existent lines is '~'
|
|
co = 0;
|
|
while (co < columns + tabstop) {
|
|
// have we gone past the end?
|
|
if (src < end) {
|
|
c = *src++;
|
|
if (c == '\n')
|
|
break;
|
|
if ((c & 0x80) && !Isprint(c)) {
|
|
c = '.';
|
|
}
|
|
if (c < ' ' || c == 0x7f) {
|
|
if (c == '\t') {
|
|
c = ' ';
|
|
// co % 8 != 7
|
|
while ((co % tabstop) != (tabstop - 1)) {
|
|
dest[co++] = c;
|
|
}
|
|
} else {
|
|
dest[co++] = '^';
|
|
if (c == 0x7f)
|
|
c = '?';
|
|
else
|
|
c += '@'; // Ctrl-X -> 'X'
|
|
}
|
|
}
|
|
}
|
|
dest[co++] = c;
|
|
// discard scrolled-off-to-the-left portion,
|
|
// in tabstop-sized pieces
|
|
if (ofs >= tabstop && co >= tabstop) {
|
|
memmove(dest, dest + tabstop, co);
|
|
co -= tabstop;
|
|
ofs -= tabstop;
|
|
}
|
|
if (src >= end)
|
|
break;
|
|
}
|
|
// check "short line, gigantic offset" case
|
|
if (co < ofs)
|
|
ofs = co;
|
|
// discard last scrolled off part
|
|
co -= ofs;
|
|
dest += ofs;
|
|
// fill the rest with spaces
|
|
if (co < columns)
|
|
memset(&dest[co], ' ', columns - co);
|
|
return dest;
|
|
}
|
|
|
|
//----- Refresh the changed screen lines -----------------------
|
|
// Copy the source line from text[] into the buffer and note
|
|
// if the current screenline is different from the new buffer.
|
|
// If they differ then that line needs redrawing on the terminal.
|
|
//
|
|
static void refresh(int full_screen)
|
|
{
|
|
#define old_offset refresh__old_offset
|
|
|
|
int li, changed;
|
|
char *tp, *sp; // pointer into text[] and screen[]
|
|
|
|
if (ENABLE_FEATURE_VI_WIN_RESIZE IF_FEATURE_VI_ASK_TERMINAL(&& !G.get_rowcol_error) ) {
|
|
unsigned c = columns, r = rows;
|
|
query_screen_dimensions();
|
|
#if ENABLE_FEATURE_VI_USE_SIGNALS
|
|
full_screen |= (c - columns) | (r - rows);
|
|
#else
|
|
if (c != columns || r != rows) {
|
|
full_screen = TRUE;
|
|
// update screen memory since SIGWINCH won't have done it
|
|
new_screen(rows, columns);
|
|
}
|
|
#endif
|
|
}
|
|
sync_cursor(dot, &crow, &ccol); // where cursor will be (on "dot")
|
|
tp = screenbegin; // index into text[] of top line
|
|
|
|
// compare text[] to screen[] and mark screen[] lines that need updating
|
|
for (li = 0; li < rows - 1; li++) {
|
|
int cs, ce; // column start & end
|
|
char *out_buf;
|
|
// format current text line
|
|
out_buf = format_line(tp /*, li*/);
|
|
|
|
// skip to the end of the current text[] line
|
|
if (tp < end) {
|
|
char *t = memchr(tp, '\n', end - tp);
|
|
if (!t) t = end - 1;
|
|
tp = t + 1;
|
|
}
|
|
|
|
// see if there are any changes between virtual screen and out_buf
|
|
changed = FALSE; // assume no change
|
|
cs = 0;
|
|
ce = columns - 1;
|
|
sp = &screen[li * columns]; // start of screen line
|
|
if (full_screen) {
|
|
// force re-draw of every single column from 0 - columns-1
|
|
goto re0;
|
|
}
|
|
// compare newly formatted buffer with virtual screen
|
|
// look forward for first difference between buf and screen
|
|
for (; cs <= ce; cs++) {
|
|
if (out_buf[cs] != sp[cs]) {
|
|
changed = TRUE; // mark for redraw
|
|
break;
|
|
}
|
|
}
|
|
|
|
// look backward for last difference between out_buf and screen
|
|
for (; ce >= cs; ce--) {
|
|
if (out_buf[ce] != sp[ce]) {
|
|
changed = TRUE; // mark for redraw
|
|
break;
|
|
}
|
|
}
|
|
// now, cs is index of first diff, and ce is index of last diff
|
|
|
|
// if horz offset has changed, force a redraw
|
|
if (offset != old_offset) {
|
|
re0:
|
|
changed = TRUE;
|
|
}
|
|
|
|
// make a sanity check of columns indexes
|
|
if (cs < 0) cs = 0;
|
|
if (ce > columns - 1) ce = columns - 1;
|
|
if (cs > ce) { cs = 0; ce = columns - 1; }
|
|
// is there a change between virtual screen and out_buf
|
|
if (changed) {
|
|
// copy changed part of buffer to virtual screen
|
|
memcpy(sp+cs, out_buf+cs, ce-cs+1);
|
|
place_cursor(li, cs);
|
|
// write line out to terminal
|
|
fwrite(&sp[cs], ce - cs + 1, 1, stdout);
|
|
}
|
|
}
|
|
|
|
place_cursor(crow, ccol);
|
|
|
|
if (!keep_index)
|
|
cindex = ccol + offset;
|
|
|
|
old_offset = offset;
|
|
#undef old_offset
|
|
}
|
|
|
|
//----- Force refresh of all Lines -----------------------------
|
|
static void redraw(int full_screen)
|
|
{
|
|
// cursor to top,left; clear to the end of screen
|
|
write1(ESC_SET_CURSOR_TOPLEFT ESC_CLEAR2EOS);
|
|
screen_erase(); // erase the internal screen buffer
|
|
last_status_cksum = 0; // force status update
|
|
refresh(full_screen); // this will redraw the entire display
|
|
show_status_line();
|
|
}
|
|
|
|
//----- Flash the screen --------------------------------------
|
|
static void flash(int h)
|
|
{
|
|
standout_start();
|
|
redraw(TRUE);
|
|
mysleep(h);
|
|
standout_end();
|
|
redraw(TRUE);
|
|
}
|
|
|
|
static void indicate_error(void)
|
|
{
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
if (crashme > 0)
|
|
return;
|
|
#endif
|
|
cmd_error = TRUE;
|
|
if (!err_method) {
|
|
write1(ESC_BELL);
|
|
} else {
|
|
flash(10);
|
|
}
|
|
}
|
|
|
|
//----- IO Routines --------------------------------------------
|
|
static int readit(void) // read (maybe cursor) key from stdin
|
|
{
|
|
int c;
|
|
|
|
fflush_all();
|
|
|
|
// Wait for input. TIMEOUT = -1 makes read_key wait even
|
|
// on nonblocking stdin.
|
|
// Note: read_key sets errno to 0 on success.
|
|
again:
|
|
c = safe_read_key(STDIN_FILENO, readbuffer, /*timeout:*/ -1);
|
|
if (c == -1) { // EOF/error
|
|
if (errno == EAGAIN) // paranoia
|
|
goto again;
|
|
go_bottom_and_clear_to_eol();
|
|
cookmode(); // terminal to "cooked"
|
|
bb_simple_error_msg_and_die("can't read user input");
|
|
}
|
|
return c;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
static int get_one_char(void)
|
|
{
|
|
int c;
|
|
|
|
if (!adding2q) {
|
|
// we are not adding to the q.
|
|
// but, we may be reading from a saved q.
|
|
// (checking "ioq" for NULL is wrong, it's not reset to NULL
|
|
// when done - "ioq_start" is reset instead).
|
|
if (ioq_start != NULL) {
|
|
// there is a queue to get chars from.
|
|
// careful with correct sign expansion!
|
|
c = (unsigned char)*ioq++;
|
|
if (c != '\0')
|
|
return c;
|
|
// the end of the q
|
|
free(ioq_start);
|
|
ioq_start = NULL;
|
|
// read from STDIN:
|
|
}
|
|
return readit();
|
|
}
|
|
// we are adding STDIN chars to q.
|
|
c = readit();
|
|
if (lmc_len >= ARRAY_SIZE(last_modifying_cmd) - 2) {
|
|
// last_modifying_cmd[] is too small, can't remember the cmd
|
|
// - drop it
|
|
adding2q = 0;
|
|
lmc_len = 0;
|
|
} else {
|
|
last_modifying_cmd[lmc_len++] = c;
|
|
}
|
|
return c;
|
|
}
|
|
#else
|
|
# define get_one_char() readit()
|
|
#endif
|
|
|
|
// Get type of thing to operate on and adjust count
|
|
static int get_motion_char(void)
|
|
{
|
|
int c, cnt;
|
|
|
|
c = get_one_char();
|
|
if (isdigit(c)) {
|
|
if (c != '0') {
|
|
// get any non-zero motion count
|
|
for (cnt = 0; isdigit(c); c = get_one_char())
|
|
cnt = cnt * 10 + (c - '0');
|
|
cmdcnt = (cmdcnt ?: 1) * cnt;
|
|
} else {
|
|
// ensure standalone '0' works
|
|
cmdcnt = 0;
|
|
}
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
// Get input line (uses "status line" area)
|
|
static char *get_input_line(const char *prompt)
|
|
{
|
|
// char [MAX_INPUT_LEN]
|
|
#define buf get_input_line__buf
|
|
|
|
int c;
|
|
int i;
|
|
|
|
strcpy(buf, prompt);
|
|
last_status_cksum = 0; // force status update
|
|
go_bottom_and_clear_to_eol();
|
|
write1(buf); // write out the :, /, or ? prompt
|
|
|
|
i = strlen(buf);
|
|
while (i < MAX_INPUT_LEN - 1) {
|
|
c = get_one_char();
|
|
if (c == '\n' || c == '\r' || c == 27)
|
|
break; // this is end of input
|
|
if (isbackspace(c)) {
|
|
// user wants to erase prev char
|
|
buf[--i] = '\0';
|
|
go_bottom_and_clear_to_eol();
|
|
if (i <= 0) // user backs up before b-o-l, exit
|
|
break;
|
|
write1(buf);
|
|
} else if (c > 0 && c < 256) { // exclude Unicode
|
|
// (TODO: need to handle Unicode)
|
|
buf[i] = c;
|
|
buf[++i] = '\0';
|
|
bb_putchar(c);
|
|
}
|
|
}
|
|
refresh(FALSE);
|
|
return buf;
|
|
#undef buf
|
|
}
|
|
|
|
static void Hit_Return(void)
|
|
{
|
|
int c;
|
|
|
|
standout_start();
|
|
write1("[Hit return to continue]");
|
|
standout_end();
|
|
while ((c = get_one_char()) != '\n' && c != '\r')
|
|
continue;
|
|
redraw(TRUE); // force redraw all
|
|
}
|
|
|
|
//----- Draw the status line at bottom of the screen -------------
|
|
// show file status on status line
|
|
static int format_edit_status(void)
|
|
{
|
|
static const char cmd_mode_indicator[] ALIGN1 = "-IR-";
|
|
|
|
#define tot format_edit_status__tot
|
|
|
|
int cur, percent, ret, trunc_at;
|
|
|
|
// modified_count is now a counter rather than a flag. this
|
|
// helps reduce the amount of line counting we need to do.
|
|
// (this will cause a mis-reporting of modified status
|
|
// once every MAXINT editing operations.)
|
|
|
|
// it would be nice to do a similar optimization here -- if
|
|
// we haven't done a motion that could have changed which line
|
|
// we're on, then we shouldn't have to do this count_lines()
|
|
cur = count_lines(text, dot);
|
|
|
|
// count_lines() is expensive.
|
|
// Call it only if something was changed since last time
|
|
// we were here:
|
|
if (modified_count != last_modified_count) {
|
|
tot = cur + count_lines(dot, end - 1) - 1;
|
|
last_modified_count = modified_count;
|
|
}
|
|
|
|
// current line percent
|
|
// ------------- ~~ ----------
|
|
// total lines 100
|
|
if (tot > 0) {
|
|
percent = (100 * cur) / tot;
|
|
} else {
|
|
cur = tot = 0;
|
|
percent = 100;
|
|
}
|
|
|
|
trunc_at = columns < STATUS_BUFFER_LEN-1 ?
|
|
columns : STATUS_BUFFER_LEN-1;
|
|
|
|
ret = snprintf(status_buffer, trunc_at+1,
|
|
#if ENABLE_FEATURE_VI_READONLY
|
|
"%c %s%s%s %d/%d %d%%",
|
|
#else
|
|
"%c %s%s %d/%d %d%%",
|
|
#endif
|
|
cmd_mode_indicator[cmd_mode & 3],
|
|
(current_filename != NULL ? current_filename : "No file"),
|
|
#if ENABLE_FEATURE_VI_READONLY
|
|
(readonly_mode ? " [Readonly]" : ""),
|
|
#endif
|
|
(modified_count ? " [Modified]" : ""),
|
|
cur, tot, percent);
|
|
|
|
if (ret >= 0 && ret < trunc_at)
|
|
return ret; // it all fit
|
|
|
|
return trunc_at; // had to truncate
|
|
#undef tot
|
|
}
|
|
|
|
static int bufsum(char *buf, int count)
|
|
{
|
|
int sum = 0;
|
|
char *e = buf + count;
|
|
while (buf < e)
|
|
sum += (unsigned char) *buf++;
|
|
return sum;
|
|
}
|
|
|
|
static void show_status_line(void)
|
|
{
|
|
int cnt = 0, cksum = 0;
|
|
|
|
// either we already have an error or status message, or we
|
|
// create one.
|
|
if (!have_status_msg) {
|
|
cnt = format_edit_status();
|
|
cksum = bufsum(status_buffer, cnt);
|
|
}
|
|
if (have_status_msg || ((cnt > 0 && last_status_cksum != cksum))) {
|
|
last_status_cksum = cksum; // remember if we have seen this line
|
|
go_bottom_and_clear_to_eol();
|
|
write1(status_buffer);
|
|
if (have_status_msg) {
|
|
if (((int)strlen(status_buffer) - (have_status_msg - 1)) >
|
|
(columns - 1) ) {
|
|
have_status_msg = 0;
|
|
Hit_Return();
|
|
}
|
|
have_status_msg = 0;
|
|
}
|
|
place_cursor(crow, ccol); // put cursor back in correct place
|
|
}
|
|
fflush_all();
|
|
}
|
|
|
|
//----- format the status buffer, the bottom line of screen ------
|
|
static void status_line(const char *format, ...)
|
|
{
|
|
va_list args;
|
|
|
|
va_start(args, format);
|
|
vsnprintf(status_buffer, STATUS_BUFFER_LEN, format, args);
|
|
va_end(args);
|
|
|
|
have_status_msg = 1;
|
|
}
|
|
static void status_line_bold(const char *format, ...)
|
|
{
|
|
va_list args;
|
|
|
|
va_start(args, format);
|
|
strcpy(status_buffer, ESC_BOLD_TEXT);
|
|
vsnprintf(status_buffer + (sizeof(ESC_BOLD_TEXT)-1),
|
|
STATUS_BUFFER_LEN - sizeof(ESC_BOLD_TEXT) - sizeof(ESC_NORM_TEXT),
|
|
format, args
|
|
);
|
|
strcat(status_buffer, ESC_NORM_TEXT);
|
|
va_end(args);
|
|
|
|
have_status_msg = 1 + (sizeof(ESC_BOLD_TEXT)-1) + (sizeof(ESC_NORM_TEXT)-1);
|
|
}
|
|
static void status_line_bold_errno(const char *fn)
|
|
{
|
|
status_line_bold("'%s' "STRERROR_FMT, fn STRERROR_ERRNO);
|
|
}
|
|
|
|
// copy s to buf, convert unprintable
|
|
static void print_literal(char *buf, const char *s)
|
|
{
|
|
char *d;
|
|
unsigned char c;
|
|
|
|
if (!s[0])
|
|
s = "(NULL)";
|
|
|
|
d = buf;
|
|
for (; *s; s++) {
|
|
c = *s;
|
|
if ((c & 0x80) && !Isprint(c))
|
|
c = '?';
|
|
if (c < ' ' || c == 0x7f) {
|
|
*d++ = '^';
|
|
c |= '@'; // 0x40
|
|
if (c == 0x7f)
|
|
c = '?';
|
|
}
|
|
*d++ = c;
|
|
*d = '\0';
|
|
if (d - buf > MAX_INPUT_LEN - 10) // paranoia
|
|
break;
|
|
}
|
|
}
|
|
static void not_implemented(const char *s)
|
|
{
|
|
char buf[MAX_INPUT_LEN];
|
|
print_literal(buf, s);
|
|
status_line_bold("'%s' is not implemented", buf);
|
|
}
|
|
|
|
//----- Block insert/delete, undo ops --------------------------
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
// copy text into a register
|
|
static char *text_yank(char *p, char *q, int dest, int buftype)
|
|
{
|
|
char *oldreg = reg[dest];
|
|
int cnt = q - p;
|
|
if (cnt < 0) { // they are backwards- reverse them
|
|
p = q;
|
|
cnt = -cnt;
|
|
}
|
|
// Don't free register yet. This prevents the memory allocator
|
|
// from reusing the free block so we can detect if it's changed.
|
|
reg[dest] = xstrndup(p, cnt + 1);
|
|
regtype[dest] = buftype;
|
|
free(oldreg);
|
|
return p;
|
|
}
|
|
|
|
static char what_reg(void)
|
|
{
|
|
char c;
|
|
|
|
c = 'D'; // default to D-reg
|
|
if (YDreg <= 25)
|
|
c = 'a' + (char) YDreg;
|
|
if (YDreg == 26)
|
|
c = 'D';
|
|
if (YDreg == 27)
|
|
c = 'U';
|
|
return c;
|
|
}
|
|
|
|
static void check_context(char cmd)
|
|
{
|
|
// Certain movement commands update the context.
|
|
if (strchr(":%{}'GHLMz/?Nn", cmd) != NULL) {
|
|
mark[27] = mark[26]; // move cur to prev
|
|
mark[26] = dot; // move local to cur
|
|
}
|
|
}
|
|
|
|
static char *swap_context(char *p) // goto new context for '' command make this the current context
|
|
{
|
|
char *tmp;
|
|
|
|
// the current context is in mark[26]
|
|
// the previous context is in mark[27]
|
|
// only swap context if other context is valid
|
|
if (text <= mark[27] && mark[27] <= end - 1) {
|
|
tmp = mark[27];
|
|
mark[27] = p;
|
|
mark[26] = p = tmp;
|
|
}
|
|
return p;
|
|
}
|
|
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
static void yank_status(const char *op, const char *p, int cnt)
|
|
{
|
|
int lines, chars;
|
|
|
|
lines = chars = 0;
|
|
while (*p) {
|
|
++chars;
|
|
if (*p++ == '\n')
|
|
++lines;
|
|
}
|
|
status_line("%s %d lines (%d chars) from [%c]",
|
|
op, lines * cnt, chars * cnt, what_reg());
|
|
}
|
|
# endif
|
|
#endif /* FEATURE_VI_YANKMARK */
|
|
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
static void undo_push(char *, unsigned, int);
|
|
#endif
|
|
|
|
// open a hole in text[]
|
|
// might reallocate text[]! use p += text_hole_make(p, ...),
|
|
// and be careful to not use pointers into potentially freed text[]!
|
|
static uintptr_t text_hole_make(char *p, int size) // at "p", make a 'size' byte hole
|
|
{
|
|
uintptr_t bias = 0;
|
|
|
|
if (size <= 0)
|
|
return bias;
|
|
end += size; // adjust the new END
|
|
if (end >= (text + text_size)) {
|
|
char *new_text;
|
|
text_size += end - (text + text_size) + 10240;
|
|
new_text = xrealloc(text, text_size);
|
|
bias = (new_text - text);
|
|
screenbegin += bias;
|
|
dot += bias;
|
|
end += bias;
|
|
p += bias;
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
{
|
|
int i;
|
|
for (i = 0; i < ARRAY_SIZE(mark); i++)
|
|
if (mark[i])
|
|
mark[i] += bias;
|
|
}
|
|
#endif
|
|
text = new_text;
|
|
}
|
|
memmove(p + size, p, end - size - p);
|
|
memset(p, ' ', size); // clear new hole
|
|
return bias;
|
|
}
|
|
|
|
// close a hole in text[] - delete "p" through "q", inclusive
|
|
// "undo" value indicates if this operation should be undo-able
|
|
#if !ENABLE_FEATURE_VI_UNDO
|
|
#define text_hole_delete(a,b,c) text_hole_delete(a,b)
|
|
#endif
|
|
static char *text_hole_delete(char *p, char *q, int undo)
|
|
{
|
|
char *src, *dest;
|
|
int cnt, hole_size;
|
|
|
|
// move forwards, from beginning
|
|
// assume p <= q
|
|
src = q + 1;
|
|
dest = p;
|
|
if (q < p) { // they are backward- swap them
|
|
src = p + 1;
|
|
dest = q;
|
|
}
|
|
hole_size = q - p + 1;
|
|
cnt = end - src;
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
switch (undo) {
|
|
case NO_UNDO:
|
|
break;
|
|
case ALLOW_UNDO:
|
|
undo_push(p, hole_size, UNDO_DEL);
|
|
break;
|
|
case ALLOW_UNDO_CHAIN:
|
|
undo_push(p, hole_size, UNDO_DEL_CHAIN);
|
|
break;
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
case ALLOW_UNDO_QUEUED:
|
|
undo_push(p, hole_size, UNDO_DEL_QUEUED);
|
|
break;
|
|
# endif
|
|
}
|
|
modified_count--;
|
|
#endif
|
|
if (src < text || src > end)
|
|
goto thd0;
|
|
if (dest < text || dest >= end)
|
|
goto thd0;
|
|
modified_count++;
|
|
if (src >= end)
|
|
goto thd_atend; // just delete the end of the buffer
|
|
memmove(dest, src, cnt);
|
|
thd_atend:
|
|
end = end - hole_size; // adjust the new END
|
|
if (dest >= end)
|
|
dest = end - 1; // make sure dest in below end-1
|
|
if (end <= text)
|
|
dest = end = text; // keep pointers valid
|
|
thd0:
|
|
return dest;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
// Flush any queued objects to the undo stack
|
|
static void undo_queue_commit(void)
|
|
{
|
|
// Pushes the queue object onto the undo stack
|
|
if (undo_q > 0) {
|
|
// Deleted character undo events grow from the end
|
|
undo_push(undo_queue + CONFIG_FEATURE_VI_UNDO_QUEUE_MAX - undo_q,
|
|
undo_q,
|
|
(undo_queue_state | UNDO_USE_SPOS)
|
|
);
|
|
undo_queue_state = UNDO_EMPTY;
|
|
undo_q = 0;
|
|
}
|
|
}
|
|
# else
|
|
# define undo_queue_commit() ((void)0)
|
|
# endif
|
|
|
|
static void flush_undo_data(void)
|
|
{
|
|
struct undo_object *undo_entry;
|
|
|
|
while (undo_stack_tail) {
|
|
undo_entry = undo_stack_tail;
|
|
undo_stack_tail = undo_entry->prev;
|
|
free(undo_entry);
|
|
}
|
|
}
|
|
|
|
// Undo functions and hooks added by Jody Bruchon (jody@jodybruchon.com)
|
|
// Add to the undo stack
|
|
static void undo_push(char *src, unsigned length, int u_type)
|
|
{
|
|
struct undo_object *undo_entry;
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
int use_spos = u_type & UNDO_USE_SPOS;
|
|
# endif
|
|
|
|
// "u_type" values
|
|
// UNDO_INS: insertion, undo will remove from buffer
|
|
// UNDO_DEL: deleted text, undo will restore to buffer
|
|
// UNDO_{INS,DEL}_CHAIN: Same as above but also calls undo_pop() when complete
|
|
// The CHAIN operations are for handling multiple operations that the user
|
|
// performs with a single action, i.e. REPLACE mode or find-and-replace commands
|
|
// UNDO_{INS,DEL}_QUEUED: If queuing feature is enabled, allow use of the queue
|
|
// for the INS/DEL operation.
|
|
// UNDO_{INS,DEL} ORed with UNDO_USE_SPOS: commit the undo queue
|
|
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
// This undo queuing functionality groups multiple character typing or backspaces
|
|
// into a single large undo object. This greatly reduces calls to malloc() for
|
|
// single-character operations while typing and has the side benefit of letting
|
|
// an undo operation remove chunks of text rather than a single character.
|
|
switch (u_type) {
|
|
case UNDO_EMPTY: // Just in case this ever happens...
|
|
return;
|
|
case UNDO_DEL_QUEUED:
|
|
if (length != 1)
|
|
return; // Only queue single characters
|
|
switch (undo_queue_state) {
|
|
case UNDO_EMPTY:
|
|
undo_queue_state = UNDO_DEL;
|
|
case UNDO_DEL:
|
|
undo_queue_spos = src;
|
|
undo_q++;
|
|
undo_queue[CONFIG_FEATURE_VI_UNDO_QUEUE_MAX - undo_q] = *src;
|
|
// If queue is full, dump it into an object
|
|
if (undo_q == CONFIG_FEATURE_VI_UNDO_QUEUE_MAX)
|
|
undo_queue_commit();
|
|
return;
|
|
case UNDO_INS:
|
|
// Switch from storing inserted text to deleted text
|
|
undo_queue_commit();
|
|
undo_push(src, length, UNDO_DEL_QUEUED);
|
|
return;
|
|
}
|
|
break;
|
|
case UNDO_INS_QUEUED:
|
|
if (length < 1)
|
|
return;
|
|
switch (undo_queue_state) {
|
|
case UNDO_EMPTY:
|
|
undo_queue_state = UNDO_INS;
|
|
undo_queue_spos = src;
|
|
case UNDO_INS:
|
|
while (length--) {
|
|
undo_q++; // Don't need to save any data for insertions
|
|
if (undo_q == CONFIG_FEATURE_VI_UNDO_QUEUE_MAX)
|
|
undo_queue_commit();
|
|
}
|
|
return;
|
|
case UNDO_DEL:
|
|
// Switch from storing deleted text to inserted text
|
|
undo_queue_commit();
|
|
undo_push(src, length, UNDO_INS_QUEUED);
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
u_type &= ~UNDO_USE_SPOS;
|
|
# endif
|
|
|
|
// Allocate a new undo object
|
|
if (u_type == UNDO_DEL || u_type == UNDO_DEL_CHAIN) {
|
|
// For UNDO_DEL objects, save deleted text
|
|
if ((text + length) == end)
|
|
length--;
|
|
// If this deletion empties text[], strip the newline. When the buffer becomes
|
|
// zero-length, a newline is added back, which requires this to compensate.
|
|
undo_entry = xzalloc(offsetof(struct undo_object, undo_text) + length);
|
|
memcpy(undo_entry->undo_text, src, length);
|
|
} else {
|
|
undo_entry = xzalloc(sizeof(*undo_entry));
|
|
}
|
|
undo_entry->length = length;
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
if (use_spos) {
|
|
undo_entry->start = undo_queue_spos - text; // use start position from queue
|
|
} else {
|
|
undo_entry->start = src - text; // use offset from start of text buffer
|
|
}
|
|
# else
|
|
undo_entry->start = src - text;
|
|
# endif
|
|
undo_entry->u_type = u_type;
|
|
|
|
// Push it on undo stack
|
|
undo_entry->prev = undo_stack_tail;
|
|
undo_stack_tail = undo_entry;
|
|
modified_count++;
|
|
}
|
|
|
|
static void undo_push_insert(char *p, int len, int undo)
|
|
{
|
|
switch (undo) {
|
|
case ALLOW_UNDO:
|
|
undo_push(p, len, UNDO_INS);
|
|
break;
|
|
case ALLOW_UNDO_CHAIN:
|
|
undo_push(p, len, UNDO_INS_CHAIN);
|
|
break;
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
case ALLOW_UNDO_QUEUED:
|
|
undo_push(p, len, UNDO_INS_QUEUED);
|
|
break;
|
|
# endif
|
|
}
|
|
}
|
|
|
|
// Undo the last operation
|
|
static void undo_pop(void)
|
|
{
|
|
int repeat;
|
|
char *u_start, *u_end;
|
|
struct undo_object *undo_entry;
|
|
|
|
// Commit pending undo queue before popping (should be unnecessary)
|
|
undo_queue_commit();
|
|
|
|
undo_entry = undo_stack_tail;
|
|
// Check for an empty undo stack
|
|
if (!undo_entry) {
|
|
status_line("Already at oldest change");
|
|
return;
|
|
}
|
|
|
|
switch (undo_entry->u_type) {
|
|
case UNDO_DEL:
|
|
case UNDO_DEL_CHAIN:
|
|
// make hole and put in text that was deleted; deallocate text
|
|
u_start = text + undo_entry->start;
|
|
text_hole_make(u_start, undo_entry->length);
|
|
memcpy(u_start, undo_entry->undo_text, undo_entry->length);
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
status_line("Undo [%d] %s %d chars at position %d",
|
|
modified_count, "restored",
|
|
undo_entry->length, undo_entry->start
|
|
);
|
|
# endif
|
|
break;
|
|
case UNDO_INS:
|
|
case UNDO_INS_CHAIN:
|
|
// delete what was inserted
|
|
u_start = undo_entry->start + text;
|
|
u_end = u_start - 1 + undo_entry->length;
|
|
text_hole_delete(u_start, u_end, NO_UNDO);
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
status_line("Undo [%d] %s %d chars at position %d",
|
|
modified_count, "deleted",
|
|
undo_entry->length, undo_entry->start
|
|
);
|
|
# endif
|
|
break;
|
|
}
|
|
repeat = 0;
|
|
switch (undo_entry->u_type) {
|
|
// If this is the end of a chain, lower modification count and refresh display
|
|
case UNDO_DEL:
|
|
case UNDO_INS:
|
|
dot = (text + undo_entry->start);
|
|
refresh(FALSE);
|
|
break;
|
|
case UNDO_DEL_CHAIN:
|
|
case UNDO_INS_CHAIN:
|
|
repeat = 1;
|
|
break;
|
|
}
|
|
// Deallocate the undo object we just processed
|
|
undo_stack_tail = undo_entry->prev;
|
|
free(undo_entry);
|
|
modified_count--;
|
|
// For chained operations, continue popping all the way down the chain.
|
|
if (repeat) {
|
|
undo_pop(); // Follow the undo chain if one exists
|
|
}
|
|
}
|
|
|
|
#else
|
|
# define flush_undo_data() ((void)0)
|
|
# define undo_queue_commit() ((void)0)
|
|
#endif /* ENABLE_FEATURE_VI_UNDO */
|
|
|
|
//----- Dot Movement Routines ----------------------------------
|
|
static void dot_left(void)
|
|
{
|
|
undo_queue_commit();
|
|
if (dot > text && dot[-1] != '\n')
|
|
dot--;
|
|
}
|
|
|
|
static void dot_right(void)
|
|
{
|
|
undo_queue_commit();
|
|
if (dot < end - 1 && *dot != '\n')
|
|
dot++;
|
|
}
|
|
|
|
static void dot_begin(void)
|
|
{
|
|
undo_queue_commit();
|
|
dot = begin_line(dot); // return pointer to first char cur line
|
|
}
|
|
|
|
static void dot_end(void)
|
|
{
|
|
undo_queue_commit();
|
|
dot = end_line(dot); // return pointer to last char cur line
|
|
}
|
|
|
|
static char *move_to_col(char *p, int l)
|
|
{
|
|
int co;
|
|
|
|
p = begin_line(p);
|
|
co = 0;
|
|
do {
|
|
if (*p == '\n') //vda || *p == '\0')
|
|
break;
|
|
co = next_column(*p, co);
|
|
} while (co <= l && p++ < end);
|
|
return p;
|
|
}
|
|
|
|
static void dot_next(void)
|
|
{
|
|
undo_queue_commit();
|
|
dot = next_line(dot);
|
|
}
|
|
|
|
static void dot_prev(void)
|
|
{
|
|
undo_queue_commit();
|
|
dot = prev_line(dot);
|
|
}
|
|
|
|
static void dot_skip_over_ws(void)
|
|
{
|
|
// skip WS
|
|
while (isspace(*dot) && *dot != '\n' && dot < end - 1)
|
|
dot++;
|
|
}
|
|
|
|
static void dot_to_char(int cmd)
|
|
{
|
|
char *q = dot;
|
|
int dir = islower(cmd) ? FORWARD : BACK;
|
|
|
|
if (last_search_char == 0)
|
|
return;
|
|
|
|
do {
|
|
do {
|
|
q += dir;
|
|
if ((dir == FORWARD ? q > end - 1 : q < text) || *q == '\n') {
|
|
indicate_error();
|
|
return;
|
|
}
|
|
} while (*q != last_search_char);
|
|
} while (--cmdcnt > 0);
|
|
|
|
dot = q;
|
|
|
|
// place cursor before/after char as required
|
|
if (cmd == 't')
|
|
dot_left();
|
|
else if (cmd == 'T')
|
|
dot_right();
|
|
}
|
|
|
|
static void dot_scroll(int cnt, int dir)
|
|
{
|
|
char *q;
|
|
|
|
undo_queue_commit();
|
|
for (; cnt > 0; cnt--) {
|
|
if (dir < 0) {
|
|
// scroll Backwards
|
|
// ctrl-Y scroll up one line
|
|
screenbegin = prev_line(screenbegin);
|
|
} else {
|
|
// scroll Forwards
|
|
// ctrl-E scroll down one line
|
|
screenbegin = next_line(screenbegin);
|
|
}
|
|
}
|
|
// make sure "dot" stays on the screen so we dont scroll off
|
|
if (dot < screenbegin)
|
|
dot = screenbegin;
|
|
q = end_screen(); // find new bottom line
|
|
if (dot > q)
|
|
dot = begin_line(q); // is dot is below bottom line?
|
|
dot_skip_over_ws();
|
|
}
|
|
|
|
static char *bound_dot(char *p) // make sure text[0] <= P < "end"
|
|
{
|
|
if (p >= end && end > text) {
|
|
p = end - 1;
|
|
indicate_error();
|
|
}
|
|
if (p < text) {
|
|
p = text;
|
|
indicate_error();
|
|
}
|
|
return p;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
static void start_new_cmd_q(char c)
|
|
{
|
|
// get buffer for new cmd
|
|
dotcnt = cmdcnt ?: 1;
|
|
last_modifying_cmd[0] = c;
|
|
lmc_len = 1;
|
|
adding2q = 1;
|
|
}
|
|
static void end_cmd_q(void)
|
|
{
|
|
# if ENABLE_FEATURE_VI_YANKMARK
|
|
YDreg = 26; // go back to default Yank/Delete reg
|
|
# endif
|
|
adding2q = 0;
|
|
}
|
|
#else
|
|
# define end_cmd_q() ((void)0)
|
|
#endif /* FEATURE_VI_DOT_CMD */
|
|
|
|
// copy text into register, then delete text.
|
|
//
|
|
#if !ENABLE_FEATURE_VI_UNDO
|
|
#define yank_delete(a,b,c,d,e) yank_delete(a,b,c,d)
|
|
#endif
|
|
static char *yank_delete(char *start, char *stop, int buftype, int yf, int undo)
|
|
{
|
|
char *p;
|
|
|
|
// make sure start <= stop
|
|
if (start > stop) {
|
|
// they are backwards, reverse them
|
|
p = start;
|
|
start = stop;
|
|
stop = p;
|
|
}
|
|
if (buftype == PARTIAL && *start == '\n')
|
|
return start;
|
|
p = start;
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
text_yank(start, stop, YDreg, buftype);
|
|
#endif
|
|
if (yf == YANKDEL) {
|
|
p = text_hole_delete(start, stop, undo);
|
|
} // delete lines
|
|
return p;
|
|
}
|
|
|
|
// might reallocate text[]!
|
|
static int file_insert(const char *fn, char *p, int initial)
|
|
{
|
|
int cnt = -1;
|
|
int fd, size;
|
|
struct stat statbuf;
|
|
|
|
if (p < text)
|
|
p = text;
|
|
if (p > end)
|
|
p = end;
|
|
|
|
fd = open(fn, O_RDONLY);
|
|
if (fd < 0) {
|
|
if (!initial)
|
|
status_line_bold_errno(fn);
|
|
return cnt;
|
|
}
|
|
|
|
// Validate file
|
|
if (fstat(fd, &statbuf) < 0) {
|
|
status_line_bold_errno(fn);
|
|
goto fi;
|
|
}
|
|
if (!S_ISREG(statbuf.st_mode)) {
|
|
status_line_bold("'%s' is not a regular file", fn);
|
|
goto fi;
|
|
}
|
|
size = (statbuf.st_size < INT_MAX ? (int)statbuf.st_size : INT_MAX);
|
|
p += text_hole_make(p, size);
|
|
cnt = full_read(fd, p, size);
|
|
if (cnt < 0) {
|
|
status_line_bold_errno(fn);
|
|
p = text_hole_delete(p, p + size - 1, NO_UNDO); // un-do buffer insert
|
|
} else if (cnt < size) {
|
|
// There was a partial read, shrink unused space
|
|
p = text_hole_delete(p + cnt, p + size - 1, NO_UNDO);
|
|
status_line_bold("can't read '%s'", fn);
|
|
}
|
|
# if ENABLE_FEATURE_VI_UNDO
|
|
else {
|
|
undo_push_insert(p, size, ALLOW_UNDO);
|
|
}
|
|
# endif
|
|
fi:
|
|
close(fd);
|
|
|
|
#if ENABLE_FEATURE_VI_READONLY
|
|
if (initial
|
|
&& ((access(fn, W_OK) < 0) ||
|
|
// root will always have access()
|
|
// so we check fileperms too
|
|
!(statbuf.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH))
|
|
)
|
|
) {
|
|
SET_READONLY_FILE(readonly_mode);
|
|
}
|
|
#endif
|
|
return cnt;
|
|
}
|
|
|
|
// find matching char of pair () [] {}
|
|
// will crash if c is not one of these
|
|
static char *find_pair(char *p, const char c)
|
|
{
|
|
const char *braces = "()[]{}";
|
|
char match;
|
|
int dir, level;
|
|
|
|
dir = strchr(braces, c) - braces;
|
|
dir ^= 1;
|
|
match = braces[dir];
|
|
dir = ((dir & 1) << 1) - 1; // 1 for ([{, -1 for )\}
|
|
|
|
// look for match, count levels of pairs (( ))
|
|
level = 1;
|
|
for (;;) {
|
|
p += dir;
|
|
if (p < text || p >= end)
|
|
return NULL;
|
|
if (*p == c)
|
|
level++; // increase pair levels
|
|
if (*p == match) {
|
|
level--; // reduce pair level
|
|
if (level == 0)
|
|
return p; // found matching pair
|
|
}
|
|
}
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
// show the matching char of a pair, () [] {}
|
|
static void showmatching(char *p)
|
|
{
|
|
char *q, *save_dot;
|
|
|
|
// we found half of a pair
|
|
q = find_pair(p, *p); // get loc of matching char
|
|
if (q == NULL) {
|
|
indicate_error(); // no matching char
|
|
} else {
|
|
// "q" now points to matching pair
|
|
save_dot = dot; // remember where we are
|
|
dot = q; // go to new loc
|
|
refresh(FALSE); // let the user see it
|
|
mysleep(40); // give user some time
|
|
dot = save_dot; // go back to old loc
|
|
refresh(FALSE);
|
|
}
|
|
}
|
|
#endif /* FEATURE_VI_SETOPTS */
|
|
|
|
// might reallocate text[]! use p += stupid_insert(p, ...),
|
|
// and be careful to not use pointers into potentially freed text[]!
|
|
static uintptr_t stupid_insert(char *p, char c) // stupidly insert the char c at 'p'
|
|
{
|
|
uintptr_t bias;
|
|
bias = text_hole_make(p, 1);
|
|
p += bias;
|
|
*p = c;
|
|
return bias;
|
|
}
|
|
|
|
// find number of characters in indent, p must be at beginning of line
|
|
static size_t indent_len(char *p)
|
|
{
|
|
char *r = p;
|
|
|
|
while (r < (end - 1) && isblank(*r))
|
|
r++;
|
|
return r - p;
|
|
}
|
|
|
|
#if !ENABLE_FEATURE_VI_UNDO
|
|
#define char_insert(a,b,c) char_insert(a,b)
|
|
#endif
|
|
static char *char_insert(char *p, char c, int undo) // insert the char c at 'p'
|
|
{
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
# define indentcol char_insert__indentcol
|
|
size_t len;
|
|
int col, ntab, nspc;
|
|
#endif
|
|
char *bol = begin_line(p);
|
|
|
|
if (c == 22) { // Is this an ctrl-V?
|
|
p += stupid_insert(p, '^'); // use ^ to indicate literal next
|
|
refresh(FALSE); // show the ^
|
|
c = get_one_char();
|
|
*p = c;
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
undo_push_insert(p, 1, undo);
|
|
#else
|
|
modified_count++;
|
|
#endif
|
|
p++;
|
|
} else if (c == 27) { // Is this an ESC?
|
|
cmd_mode = 0;
|
|
undo_queue_commit();
|
|
cmdcnt = 0;
|
|
end_cmd_q(); // stop adding to q
|
|
last_status_cksum = 0; // force status update
|
|
if ((dot > text) && (p[-1] != '\n')) {
|
|
p--;
|
|
}
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
if (autoindent) {
|
|
len = indent_len(bol);
|
|
col = get_column(bol + len);
|
|
if (len && col == indentcol && bol[len] == '\n') {
|
|
// remove autoindent from otherwise empty line
|
|
text_hole_delete(bol, bol + len - 1, undo);
|
|
p = bol;
|
|
}
|
|
}
|
|
#endif
|
|
} else if (c == 4) { // ctrl-D reduces indentation
|
|
char *r = bol + indent_len(bol);
|
|
int prev = prev_tabstop(get_column(r));
|
|
while (r > bol && get_column(r) > prev) {
|
|
if (p > bol)
|
|
p--;
|
|
r--;
|
|
r = text_hole_delete(r, r, ALLOW_UNDO_QUEUED);
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
if (autoindent && indentcol && r == end_line(p)) {
|
|
// record changed size of autoindent
|
|
indentcol = get_column(p);
|
|
return p;
|
|
}
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
} else if (c == '\t' && expandtab) { // expand tab
|
|
col = get_column(p);
|
|
col = next_tabstop(col) - col + 1;
|
|
while (col--) {
|
|
# if ENABLE_FEATURE_VI_UNDO
|
|
undo_push_insert(p, 1, undo);
|
|
# else
|
|
modified_count++;
|
|
# endif
|
|
p += 1 + stupid_insert(p, ' ');
|
|
}
|
|
#endif
|
|
} else if (isbackspace(c)) {
|
|
if (cmd_mode == 2) {
|
|
// special treatment for backspace in Replace mode
|
|
if (p > rstart) {
|
|
p--;
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
undo_pop();
|
|
#endif
|
|
}
|
|
} else if (p > text) {
|
|
p--;
|
|
p = text_hole_delete(p, p, ALLOW_UNDO_QUEUED); // shrink buffer 1 char
|
|
}
|
|
} else {
|
|
// insert a char into text[]
|
|
if (c == 13)
|
|
c = '\n'; // translate \r to \n
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
if (c == '\n')
|
|
undo_queue_commit();
|
|
# endif
|
|
undo_push_insert(p, 1, undo);
|
|
#else
|
|
modified_count++;
|
|
#endif
|
|
p += 1 + stupid_insert(p, c); // insert the char
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
if (showmatch && strchr(")]}", c) != NULL) {
|
|
showmatching(p - 1);
|
|
}
|
|
if (autoindent && c == '\n') { // auto indent the new line
|
|
if (newindent < 0) {
|
|
// use indent of previous line
|
|
bol = prev_line(p);
|
|
len = indent_len(bol);
|
|
col = get_column(bol + len);
|
|
|
|
if (len && col == indentcol) {
|
|
// previous line was empty except for autoindent
|
|
// move the indent to the current line
|
|
memmove(bol + 1, bol, len);
|
|
*bol = '\n';
|
|
return p;
|
|
}
|
|
} else {
|
|
// for 'O'/'cc' commands add indent before newly inserted NL
|
|
if (p != end - 1) // but not for 'cc' at EOF
|
|
p--;
|
|
col = newindent;
|
|
}
|
|
|
|
if (col) {
|
|
// only record indent if in insert/replace mode or for
|
|
// the 'o'/'O'/'cc' commands, which are switched to
|
|
// insert mode early.
|
|
indentcol = cmd_mode != 0 ? col : 0;
|
|
if (expandtab) {
|
|
ntab = 0;
|
|
nspc = col;
|
|
} else {
|
|
ntab = col / tabstop;
|
|
nspc = col % tabstop;
|
|
}
|
|
p += text_hole_make(p, ntab + nspc);
|
|
# if ENABLE_FEATURE_VI_UNDO
|
|
undo_push_insert(p, ntab + nspc, undo);
|
|
# endif
|
|
memset(p, '\t', ntab);
|
|
p += ntab;
|
|
memset(p, ' ', nspc);
|
|
return p + nspc;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
indentcol = 0;
|
|
# undef indentcol
|
|
#endif
|
|
return p;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_COLON_EXPAND
|
|
static void init_filename(char *fn)
|
|
{
|
|
char *copy = xstrdup(fn);
|
|
|
|
if (current_filename == NULL) {
|
|
current_filename = copy;
|
|
} else {
|
|
free(alt_filename);
|
|
alt_filename = copy;
|
|
}
|
|
}
|
|
#else
|
|
# define init_filename(f) ((void)(0))
|
|
#endif
|
|
|
|
static void update_filename(char *fn)
|
|
{
|
|
#if ENABLE_FEATURE_VI_COLON_EXPAND
|
|
if (fn == NULL)
|
|
return;
|
|
|
|
if (current_filename == NULL || strcmp(fn, current_filename) != 0) {
|
|
free(alt_filename);
|
|
alt_filename = current_filename;
|
|
current_filename = xstrdup(fn);
|
|
}
|
|
#else
|
|
if (fn != current_filename) {
|
|
free(current_filename);
|
|
current_filename = xstrdup(fn);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// read text from file or create an empty buf
|
|
// will also update current_filename
|
|
static int init_text_buffer(char *fn)
|
|
{
|
|
int rc;
|
|
|
|
// allocate/reallocate text buffer
|
|
free(text);
|
|
text_size = 10240;
|
|
screenbegin = dot = end = text = xzalloc(text_size);
|
|
|
|
update_filename(fn);
|
|
rc = file_insert(fn, text, 1);
|
|
if (rc < 0) {
|
|
// file doesnt exist. Start empty buf with dummy line
|
|
char_insert(text, '\n', NO_UNDO);
|
|
}
|
|
|
|
flush_undo_data();
|
|
modified_count = 0;
|
|
last_modified_count = -1;
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
// init the marks
|
|
memset(mark, 0, sizeof(mark));
|
|
#endif
|
|
return rc;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_YANKMARK \
|
|
|| (ENABLE_FEATURE_VI_COLON && ENABLE_FEATURE_VI_SEARCH) \
|
|
|| ENABLE_FEATURE_VI_CRASHME
|
|
// might reallocate text[]! use p += string_insert(p, ...),
|
|
// and be careful to not use pointers into potentially freed text[]!
|
|
# if !ENABLE_FEATURE_VI_UNDO
|
|
# define string_insert(a,b,c) string_insert(a,b)
|
|
# endif
|
|
static uintptr_t string_insert(char *p, const char *s, int undo) // insert the string at 'p'
|
|
{
|
|
uintptr_t bias;
|
|
int i;
|
|
|
|
i = strlen(s);
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
undo_push_insert(p, i, undo);
|
|
#endif
|
|
bias = text_hole_make(p, i);
|
|
p += bias;
|
|
memcpy(p, s, i);
|
|
return bias;
|
|
}
|
|
#endif
|
|
|
|
static int file_write(char *fn, char *first, char *last)
|
|
{
|
|
int fd, cnt, charcnt;
|
|
|
|
if (fn == 0) {
|
|
status_line_bold("No current filename");
|
|
return -2;
|
|
}
|
|
// By popular request we do not open file with O_TRUNC,
|
|
// but instead ftruncate() it _after_ successful write.
|
|
// Might reduce amount of data lost on power fail etc.
|
|
fd = open(fn, (O_WRONLY | O_CREAT), 0666);
|
|
if (fd < 0)
|
|
return -1;
|
|
cnt = last - first + 1;
|
|
charcnt = full_write(fd, first, cnt);
|
|
ftruncate(fd, charcnt);
|
|
if (charcnt == cnt) {
|
|
// good write
|
|
//modified_count = FALSE;
|
|
} else {
|
|
charcnt = 0;
|
|
}
|
|
close(fd);
|
|
return charcnt;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_SEARCH
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
// search for pattern starting at p
|
|
static char *char_search(char *p, const char *pat, int dir_and_range)
|
|
{
|
|
struct re_pattern_buffer preg;
|
|
const char *err;
|
|
char *q;
|
|
int i, size, range, start;
|
|
|
|
re_syntax_options = RE_SYNTAX_POSIX_BASIC & (~RE_DOT_NEWLINE);
|
|
if (ignorecase)
|
|
re_syntax_options |= RE_ICASE;
|
|
|
|
memset(&preg, 0, sizeof(preg));
|
|
err = re_compile_pattern(pat, strlen(pat), &preg);
|
|
preg.not_bol = p != text;
|
|
preg.not_eol = p != end - 1;
|
|
if (err != NULL) {
|
|
status_line_bold("bad search pattern '%s': %s", pat, err);
|
|
return p;
|
|
}
|
|
|
|
range = (dir_and_range & 1);
|
|
q = end - 1; // if FULL
|
|
if (range == LIMITED)
|
|
q = next_line(p);
|
|
if (dir_and_range < 0) { // BACK?
|
|
q = text;
|
|
if (range == LIMITED)
|
|
q = prev_line(p);
|
|
}
|
|
|
|
// RANGE could be negative if we are searching backwards
|
|
range = q - p;
|
|
if (range < 0) {
|
|
size = -range;
|
|
start = size;
|
|
} else {
|
|
size = range;
|
|
start = 0;
|
|
}
|
|
q = p - start;
|
|
if (q < text)
|
|
q = text;
|
|
// search for the compiled pattern, preg, in p[]
|
|
// range < 0, start == size: search backward
|
|
// range > 0, start == 0: search forward
|
|
// re_search() < 0: not found or error
|
|
// re_search() >= 0: index of found pattern
|
|
// struct pattern char int int int struct reg
|
|
// re_search(*pattern_buffer, *string, size, start, range, *regs)
|
|
i = re_search(&preg, q, size, start, range, /*struct re_registers*:*/ NULL);
|
|
regfree(&preg);
|
|
return i < 0 ? NULL : q + i;
|
|
}
|
|
# else
|
|
# if ENABLE_FEATURE_VI_SETOPTS
|
|
static int mycmp(const char *s1, const char *s2, int len)
|
|
{
|
|
if (ignorecase) {
|
|
return strncasecmp(s1, s2, len);
|
|
}
|
|
return strncmp(s1, s2, len);
|
|
}
|
|
# else
|
|
# define mycmp strncmp
|
|
# endif
|
|
static char *char_search(char *p, const char *pat, int dir_and_range)
|
|
{
|
|
char *start, *stop;
|
|
int len;
|
|
int range;
|
|
|
|
len = strlen(pat);
|
|
range = (dir_and_range & 1);
|
|
if (dir_and_range > 0) { //FORWARD?
|
|
stop = end - 1; // assume range is p..end-1
|
|
if (range == LIMITED)
|
|
stop = next_line(p); // range is to next line
|
|
for (start = p; start < stop; start++) {
|
|
if (mycmp(start, pat, len) == 0) {
|
|
return start;
|
|
}
|
|
}
|
|
} else { //BACK
|
|
stop = text; // assume range is text..p
|
|
if (range == LIMITED)
|
|
stop = prev_line(p); // range is to prev line
|
|
for (start = p - len; start >= stop; start--) {
|
|
if (mycmp(start, pat, len) == 0) {
|
|
return start;
|
|
}
|
|
}
|
|
}
|
|
// pattern not found
|
|
return NULL;
|
|
}
|
|
# endif
|
|
#endif /* FEATURE_VI_SEARCH */
|
|
|
|
//----- The Colon commands -------------------------------------
|
|
#if ENABLE_FEATURE_VI_COLON
|
|
// Evaluate colon address expression. Returns a pointer to the
|
|
// next character or NULL on error. If 'result' contains a valid
|
|
// address 'valid' is TRUE.
|
|
static char *get_one_address(char *p, int *result, int *valid)
|
|
{
|
|
int num, sign, addr, got_addr;
|
|
# if ENABLE_FEATURE_VI_YANKMARK || ENABLE_FEATURE_VI_SEARCH
|
|
char *q, c;
|
|
# endif
|
|
IF_FEATURE_VI_SEARCH(int dir;)
|
|
|
|
got_addr = FALSE;
|
|
addr = count_lines(text, dot); // default to current line
|
|
sign = 0;
|
|
for (;;) {
|
|
if (isblank(*p)) {
|
|
if (got_addr) {
|
|
addr += sign;
|
|
sign = 0;
|
|
}
|
|
p++;
|
|
} else if (!got_addr && *p == '.') { // the current line
|
|
p++;
|
|
//addr = count_lines(text, dot);
|
|
got_addr = TRUE;
|
|
} else if (!got_addr && *p == '$') { // the last line in file
|
|
p++;
|
|
addr = count_lines(text, end - 1);
|
|
got_addr = TRUE;
|
|
}
|
|
# if ENABLE_FEATURE_VI_YANKMARK
|
|
else if (!got_addr && *p == '\'') { // is this a mark addr
|
|
p++;
|
|
c = tolower(*p);
|
|
p++;
|
|
q = NULL;
|
|
if (c >= 'a' && c <= 'z') {
|
|
// we have a mark
|
|
c = c - 'a';
|
|
q = mark[(unsigned char) c];
|
|
}
|
|
if (q == NULL) { // is mark valid
|
|
status_line_bold("Mark not set");
|
|
return NULL;
|
|
}
|
|
addr = count_lines(text, q);
|
|
got_addr = TRUE;
|
|
}
|
|
# endif
|
|
# if ENABLE_FEATURE_VI_SEARCH
|
|
else if (!got_addr && (*p == '/' || *p == '?')) { // a search pattern
|
|
c = *p;
|
|
q = strchrnul(p + 1, c);
|
|
if (p + 1 != q) {
|
|
// save copy of new pattern
|
|
free(last_search_pattern);
|
|
last_search_pattern = xstrndup(p, q - p);
|
|
}
|
|
p = q;
|
|
if (*p == c)
|
|
p++;
|
|
if (c == '/') {
|
|
q = next_line(dot);
|
|
dir = (FORWARD << 1) | FULL;
|
|
} else {
|
|
q = begin_line(dot);
|
|
dir = ((unsigned)BACK << 1) | FULL;
|
|
}
|
|
q = char_search(q, last_search_pattern + 1, dir);
|
|
if (q == NULL) {
|
|
// no match, continue from other end of file
|
|
q = char_search(dir > 0 ? text : end - 1,
|
|
last_search_pattern + 1, dir);
|
|
if (q == NULL) {
|
|
status_line_bold("Pattern not found");
|
|
return NULL;
|
|
}
|
|
}
|
|
addr = count_lines(text, q);
|
|
got_addr = TRUE;
|
|
}
|
|
# endif
|
|
else if (isdigit(*p)) {
|
|
num = 0;
|
|
while (isdigit(*p))
|
|
num = num * 10 + *p++ -'0';
|
|
if (!got_addr) { // specific line number
|
|
addr = num;
|
|
got_addr = TRUE;
|
|
} else { // offset from current addr
|
|
addr += sign >= 0 ? num : -num;
|
|
}
|
|
sign = 0;
|
|
} else if (*p == '-' || *p == '+') {
|
|
if (!got_addr) { // default address is dot
|
|
//addr = count_lines(text, dot);
|
|
got_addr = TRUE;
|
|
} else {
|
|
addr += sign;
|
|
}
|
|
sign = *p++ == '-' ? -1 : 1;
|
|
} else {
|
|
addr += sign; // consume unused trailing sign
|
|
break;
|
|
}
|
|
}
|
|
*result = addr;
|
|
*valid = got_addr;
|
|
return p;
|
|
}
|
|
|
|
# define GET_ADDRESS 0
|
|
# define GET_SEPARATOR 1
|
|
|
|
// Read line addresses for a colon command. The user can enter as
|
|
// many as they like but only the last two will be used.
|
|
static char *get_address(char *p, int *b, int *e, unsigned int *got)
|
|
{
|
|
int state = GET_ADDRESS;
|
|
int valid;
|
|
int addr;
|
|
char *save_dot = dot;
|
|
|
|
//----- get the address' i.e., 1,3 'a,'b -----
|
|
for (;;) {
|
|
if (isblank(*p)) {
|
|
p++;
|
|
} else if (state == GET_ADDRESS && *p == '%') { // alias for 1,$
|
|
p++;
|
|
*b = 1;
|
|
*e = count_lines(text, end-1);
|
|
*got = 3;
|
|
state = GET_SEPARATOR;
|
|
} else if (state == GET_ADDRESS) {
|
|
valid = FALSE;
|
|
p = get_one_address(p, &addr, &valid);
|
|
// Quit on error or if the address is invalid and isn't of
|
|
// the form ',$' or '1,' (in which case it defaults to dot).
|
|
if (p == NULL || !(valid || *p == ',' || *p == ';' || *got & 1))
|
|
break;
|
|
*b = *e;
|
|
*e = addr;
|
|
*got = (*got << 1) | 1;
|
|
state = GET_SEPARATOR;
|
|
} else if (state == GET_SEPARATOR && (*p == ',' || *p == ';')) {
|
|
if (*p == ';')
|
|
dot = find_line(*e);
|
|
p++;
|
|
state = GET_ADDRESS;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
dot = save_dot;
|
|
return p;
|
|
}
|
|
|
|
# if ENABLE_FEATURE_VI_SET && ENABLE_FEATURE_VI_SETOPTS
|
|
static void setops(char *args, int flg_no)
|
|
{
|
|
char *eq;
|
|
int index;
|
|
|
|
eq = strchr(args, '=');
|
|
if (eq) *eq = '\0';
|
|
index = index_in_strings(OPTS_STR, args + flg_no);
|
|
if (eq) *eq = '=';
|
|
if (index < 0) {
|
|
bad:
|
|
status_line_bold("bad option: %s", args);
|
|
return;
|
|
}
|
|
|
|
index = 1 << (index >> 1); // convert to VI_bit
|
|
|
|
if (index & VI_TABSTOP) {
|
|
int t;
|
|
if (!eq || flg_no) // no "=NNN" or it is "notabstop"?
|
|
goto bad;
|
|
t = bb_strtou(eq + 1, NULL, 10);
|
|
if (t <= 0 || t > MAX_TABSTOP)
|
|
goto bad;
|
|
tabstop = t;
|
|
return;
|
|
}
|
|
if (eq) goto bad; // boolean option has "="?
|
|
if (flg_no) {
|
|
vi_setops &= ~index;
|
|
} else {
|
|
vi_setops |= index;
|
|
}
|
|
}
|
|
# endif
|
|
|
|
# if ENABLE_FEATURE_VI_COLON_EXPAND
|
|
static char *expand_args(char *args)
|
|
{
|
|
char *s, *t;
|
|
const char *replace;
|
|
|
|
args = xstrdup(args);
|
|
for (s = args; *s; s++) {
|
|
if (*s == '%') {
|
|
replace = current_filename;
|
|
} else if (*s == '#') {
|
|
replace = alt_filename;
|
|
} else {
|
|
if (*s == '\\' && s[1] != '\0') {
|
|
for (t = s++; *t; t++)
|
|
*t = t[1];
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (replace == NULL) {
|
|
free(args);
|
|
status_line_bold("No previous filename");
|
|
return NULL;
|
|
}
|
|
|
|
*s = '\0';
|
|
t = xasprintf("%s%s%s", args, replace, s+1);
|
|
s = t + (s - args) + strlen(replace);
|
|
free(args);
|
|
args = t;
|
|
}
|
|
return args;
|
|
}
|
|
# else
|
|
# define expand_args(a) (a)
|
|
# endif
|
|
#endif /* FEATURE_VI_COLON */
|
|
|
|
#if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
# define MAX_SUBPATTERN 10 // subpatterns \0 .. \9
|
|
|
|
// Like strchr() but skipping backslash-escaped characters
|
|
static char *strchr_backslash(const char *s, int c)
|
|
{
|
|
while (*s) {
|
|
if (*s == c)
|
|
return (char *)s;
|
|
if (*s == '\\')
|
|
if (*++s == '\0')
|
|
break;
|
|
s++;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// If the return value is not NULL the caller should free R
|
|
static char *regex_search(char *q, regex_t *preg, const char *Rorig,
|
|
size_t *len_F, size_t *len_R, char **R)
|
|
{
|
|
regmatch_t regmatch[MAX_SUBPATTERN], *cur_match;
|
|
char *found = NULL;
|
|
const char *t;
|
|
char *r;
|
|
|
|
regmatch[0].rm_so = 0;
|
|
regmatch[0].rm_eo = end_line(q) - q;
|
|
if (regexec(preg, q, MAX_SUBPATTERN, regmatch, REG_STARTEND) != 0)
|
|
return found;
|
|
|
|
found = q + regmatch[0].rm_so;
|
|
*len_F = regmatch[0].rm_eo - regmatch[0].rm_so;
|
|
*R = NULL;
|
|
|
|
fill_result:
|
|
// first pass calculates len_R, second fills R
|
|
*len_R = 0;
|
|
for (t = Rorig, r = *R; *t; t++) {
|
|
size_t len = 1; // default is to copy one char from replace pattern
|
|
const char *from = t;
|
|
if (*t == '\\') {
|
|
from = ++t; // skip backslash
|
|
if (*t >= '0' && *t < '0' + MAX_SUBPATTERN) {
|
|
cur_match = regmatch + (*t - '0');
|
|
if (cur_match->rm_so >= 0) {
|
|
len = cur_match->rm_eo - cur_match->rm_so;
|
|
from = q + cur_match->rm_so;
|
|
}
|
|
}
|
|
}
|
|
*len_R += len;
|
|
if (*R) {
|
|
memcpy(r, from, len);
|
|
r += len;
|
|
/* *r = '\0'; - xzalloc did it */
|
|
}
|
|
}
|
|
if (*R == NULL) {
|
|
*R = xzalloc(*len_R + 1);
|
|
goto fill_result;
|
|
}
|
|
|
|
return found;
|
|
}
|
|
#else /* !ENABLE_FEATURE_VI_REGEX_SEARCH */
|
|
# define strchr_backslash(s, c) strchr(s, c)
|
|
#endif /* ENABLE_FEATURE_VI_REGEX_SEARCH */
|
|
|
|
// buf must be no longer than MAX_INPUT_LEN!
|
|
static void colon(char *buf)
|
|
{
|
|
#if !ENABLE_FEATURE_VI_COLON
|
|
// Simple ":cmd" handler with minimal set of commands
|
|
char *p = buf;
|
|
int cnt;
|
|
|
|
if (*p == ':')
|
|
p++;
|
|
cnt = strlen(p);
|
|
if (cnt == 0)
|
|
return;
|
|
if (strncmp(p, "quit", cnt) == 0
|
|
|| strcmp(p, "q!") == 0
|
|
) {
|
|
if (modified_count && p[1] != '!') {
|
|
status_line_bold("No write since last change (:%s! overrides)", p);
|
|
} else {
|
|
editing = 0;
|
|
}
|
|
return;
|
|
}
|
|
if (strncmp(p, "write", cnt) == 0
|
|
|| strcmp(p, "wq") == 0
|
|
|| strcmp(p, "wn") == 0
|
|
|| (p[0] == 'x' && !p[1])
|
|
) {
|
|
if (modified_count != 0 || p[0] != 'x') {
|
|
cnt = file_write(current_filename, text, end - 1);
|
|
}
|
|
if (cnt < 0) {
|
|
if (cnt == -1)
|
|
status_line_bold("Write error: "STRERROR_FMT STRERROR_ERRNO);
|
|
} else {
|
|
modified_count = 0;
|
|
last_modified_count = -1;
|
|
status_line("'%s' %uL, %uC",
|
|
current_filename,
|
|
count_lines(text, end - 1), cnt
|
|
);
|
|
if (p[0] == 'x'
|
|
|| p[1] == 'q' || p[1] == 'n'
|
|
) {
|
|
editing = 0;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (strncmp(p, "file", cnt) == 0) {
|
|
last_status_cksum = 0; // force status update
|
|
return;
|
|
}
|
|
if (sscanf(p, "%d", &cnt) > 0) {
|
|
dot = find_line(cnt);
|
|
dot_skip_over_ws();
|
|
return;
|
|
}
|
|
not_implemented(p);
|
|
#else
|
|
|
|
// check how many addresses we got
|
|
# define GOT_ADDRESS (got & 1)
|
|
# define GOT_RANGE ((got & 3) == 3)
|
|
|
|
char c, *buf1, *q, *r;
|
|
char *fn, cmd[MAX_INPUT_LEN], *cmdend, *args, *exp = NULL;
|
|
int i, l, li, b, e;
|
|
unsigned int got;
|
|
int useforce;
|
|
|
|
// :3154 // if (-e line 3154) goto it else stay put
|
|
// :4,33w! foo // write a portion of buffer to file "foo"
|
|
// :w // write all of buffer to current file
|
|
// :q // quit
|
|
// :q! // quit- dont care about modified file
|
|
// :'a,'z!sort -u // filter block through sort
|
|
// :'f // goto mark "f"
|
|
// :'fl // list literal the mark "f" line
|
|
// :.r bar // read file "bar" into buffer before dot
|
|
// :/123/,/abc/d // delete lines from "123" line to "abc" line
|
|
// :/xyz/ // goto the "xyz" line
|
|
// :s/find/replace/ // substitute pattern "find" with "replace"
|
|
// :!<cmd> // run <cmd> then return
|
|
//
|
|
|
|
while (*buf == ':')
|
|
buf++; // move past leading colons
|
|
while (isblank(*buf))
|
|
buf++; // move past leading blanks
|
|
if (!buf[0] || buf[0] == '"')
|
|
goto ret; // ignore empty lines or those starting with '"'
|
|
|
|
li = i = 0;
|
|
b = e = -1;
|
|
got = 0;
|
|
li = count_lines(text, end - 1);
|
|
fn = current_filename;
|
|
|
|
// look for optional address(es) :. :1 :1,9 :'q,'a :%
|
|
buf = get_address(buf, &b, &e, &got);
|
|
if (buf == NULL) {
|
|
goto ret;
|
|
}
|
|
|
|
// get the COMMAND into cmd[]
|
|
strcpy(cmd, buf);
|
|
buf1 = cmd;
|
|
while (!isspace(*buf1) && *buf1 != '\0') {
|
|
buf1++;
|
|
}
|
|
cmdend = buf1;
|
|
// get any ARGuments
|
|
while (isblank(*buf1))
|
|
buf1++;
|
|
args = buf1;
|
|
*cmdend = '\0';
|
|
useforce = FALSE;
|
|
if (cmdend > cmd && cmdend[-1] == '!') {
|
|
useforce = TRUE;
|
|
cmdend[-1] = '\0'; // get rid of !
|
|
}
|
|
// assume the command will want a range, certain commands
|
|
// (read, substitute) need to adjust these assumptions
|
|
if (!GOT_ADDRESS) {
|
|
q = text; // no addr, use 1,$ for the range
|
|
r = end - 1;
|
|
} else {
|
|
// at least one addr was given, get its details
|
|
if (e < 0 || e > li) {
|
|
status_line_bold("Invalid range");
|
|
goto ret;
|
|
}
|
|
q = r = find_line(e);
|
|
if (!GOT_RANGE) {
|
|
// if there is only one addr, then it's the line
|
|
// number of the single line the user wants.
|
|
// Reset the end pointer to the end of that line.
|
|
r = end_line(q);
|
|
li = 1;
|
|
} else {
|
|
// we were given two addrs. change the
|
|
// start pointer to the addr given by user.
|
|
if (b < 0 || b > li || b > e) {
|
|
status_line_bold("Invalid range");
|
|
goto ret;
|
|
}
|
|
q = find_line(b); // what line is #b
|
|
r = end_line(r);
|
|
li = e - b + 1;
|
|
}
|
|
}
|
|
// ------------ now look for the command ------------
|
|
i = strlen(cmd);
|
|
if (i == 0) { // :123CR goto line #123
|
|
if (e >= 0) {
|
|
dot = find_line(e); // what line is #e
|
|
dot_skip_over_ws();
|
|
}
|
|
}
|
|
# if ENABLE_FEATURE_ALLOW_EXEC
|
|
else if (cmd[0] == '!') { // run a cmd
|
|
int retcode;
|
|
// :!ls run the <cmd>
|
|
exp = expand_args(buf + 1);
|
|
if (exp == NULL)
|
|
goto ret;
|
|
go_bottom_and_clear_to_eol();
|
|
cookmode();
|
|
retcode = system(exp); // run the cmd
|
|
if (retcode)
|
|
printf("\nshell returned %i\n\n", retcode);
|
|
rawmode();
|
|
Hit_Return(); // let user see results
|
|
}
|
|
# endif
|
|
else if (cmd[0] == '=' && !cmd[1]) { // where is the address
|
|
if (!GOT_ADDRESS) { // no addr given- use defaults
|
|
e = count_lines(text, dot);
|
|
}
|
|
status_line("%d", e);
|
|
} else if (strncmp(cmd, "delete", i) == 0) { // delete lines
|
|
if (!GOT_ADDRESS) { // no addr given- use defaults
|
|
q = begin_line(dot); // assume .,. for the range
|
|
r = end_line(dot);
|
|
}
|
|
dot = yank_delete(q, r, WHOLE, YANKDEL, ALLOW_UNDO); // save, then delete lines
|
|
dot_skip_over_ws();
|
|
} else if (strncmp(cmd, "edit", i) == 0) { // Edit a file
|
|
int size;
|
|
|
|
// don't edit, if the current file has been modified
|
|
if (modified_count && !useforce) {
|
|
status_line_bold("No write since last change (:%s! overrides)", cmd);
|
|
goto ret;
|
|
}
|
|
if (args[0]) {
|
|
// the user supplied a file name
|
|
fn = exp = expand_args(args);
|
|
if (exp == NULL)
|
|
goto ret;
|
|
} else if (current_filename == NULL) {
|
|
// no user file name, no current name- punt
|
|
status_line_bold("No current filename");
|
|
goto ret;
|
|
}
|
|
|
|
size = init_text_buffer(fn);
|
|
|
|
# if ENABLE_FEATURE_VI_YANKMARK
|
|
if (Ureg >= 0 && Ureg < 28) {
|
|
free(reg[Ureg]); // free orig line reg- for 'U'
|
|
reg[Ureg] = NULL;
|
|
}
|
|
/*if (YDreg < 28) - always true*/ {
|
|
free(reg[YDreg]); // free default yank/delete register
|
|
reg[YDreg] = NULL;
|
|
}
|
|
# endif
|
|
// how many lines in text[]?
|
|
li = count_lines(text, end - 1);
|
|
status_line("'%s'%s"
|
|
IF_FEATURE_VI_READONLY("%s")
|
|
" %uL, %uC",
|
|
fn,
|
|
(size < 0 ? " [New file]" : ""),
|
|
IF_FEATURE_VI_READONLY(
|
|
((readonly_mode) ? " [Readonly]" : ""),
|
|
)
|
|
li, (int)(end - text)
|
|
);
|
|
} else if (strncmp(cmd, "file", i) == 0) { // what File is this
|
|
if (e >= 0) {
|
|
status_line_bold("No address allowed on this command");
|
|
goto ret;
|
|
}
|
|
if (args[0]) {
|
|
// user wants a new filename
|
|
exp = expand_args(args);
|
|
if (exp == NULL)
|
|
goto ret;
|
|
update_filename(exp);
|
|
} else {
|
|
// user wants file status info
|
|
last_status_cksum = 0; // force status update
|
|
}
|
|
} else if (strncmp(cmd, "features", i) == 0) { // what features are available
|
|
// print out values of all features
|
|
go_bottom_and_clear_to_eol();
|
|
cookmode();
|
|
show_help();
|
|
rawmode();
|
|
Hit_Return();
|
|
} else if (strncmp(cmd, "list", i) == 0) { // literal print line
|
|
if (!GOT_ADDRESS) { // no addr given- use defaults
|
|
q = begin_line(dot); // assume .,. for the range
|
|
r = end_line(dot);
|
|
}
|
|
go_bottom_and_clear_to_eol();
|
|
puts("\r");
|
|
for (; q <= r; q++) {
|
|
int c_is_no_print;
|
|
|
|
c = *q;
|
|
c_is_no_print = (c & 0x80) && !Isprint(c);
|
|
if (c_is_no_print) {
|
|
c = '.';
|
|
standout_start();
|
|
}
|
|
if (c == '\n') {
|
|
write1("$\r");
|
|
} else if (c < ' ' || c == 127) {
|
|
bb_putchar('^');
|
|
if (c == 127)
|
|
c = '?';
|
|
else
|
|
c += '@';
|
|
}
|
|
bb_putchar(c);
|
|
if (c_is_no_print)
|
|
standout_end();
|
|
}
|
|
Hit_Return();
|
|
} else if (strncmp(cmd, "quit", i) == 0 // quit
|
|
|| strncmp(cmd, "next", i) == 0 // edit next file
|
|
|| strncmp(cmd, "prev", i) == 0 // edit previous file
|
|
) {
|
|
int n;
|
|
if (useforce) {
|
|
if (*cmd == 'q') {
|
|
// force end of argv list
|
|
optind = cmdline_filecnt;
|
|
}
|
|
editing = 0;
|
|
goto ret;
|
|
}
|
|
// don't exit if the file been modified
|
|
if (modified_count) {
|
|
status_line_bold("No write since last change (:%s! overrides)", cmd);
|
|
goto ret;
|
|
}
|
|
// are there other file to edit
|
|
n = cmdline_filecnt - optind - 1;
|
|
if (*cmd == 'q' && n > 0) {
|
|
status_line_bold("%u more file(s) to edit", n);
|
|
goto ret;
|
|
}
|
|
if (*cmd == 'n' && n <= 0) {
|
|
status_line_bold("No more files to edit");
|
|
goto ret;
|
|
}
|
|
if (*cmd == 'p') {
|
|
// are there previous files to edit
|
|
if (optind < 1) {
|
|
status_line_bold("No previous files to edit");
|
|
goto ret;
|
|
}
|
|
optind -= 2;
|
|
}
|
|
editing = 0;
|
|
} else if (strncmp(cmd, "read", i) == 0) { // read file into text[]
|
|
int size, num;
|
|
|
|
if (args[0]) {
|
|
// the user supplied a file name
|
|
fn = exp = expand_args(args);
|
|
if (exp == NULL)
|
|
goto ret;
|
|
init_filename(fn);
|
|
} else if (current_filename == NULL) {
|
|
// no user file name, no current name- punt
|
|
status_line_bold("No current filename");
|
|
goto ret;
|
|
}
|
|
if (e == 0) { // user said ":0r foo"
|
|
q = text;
|
|
} else { // read after given line or current line if none given
|
|
q = next_line(GOT_ADDRESS ? find_line(e) : dot);
|
|
// read after last line
|
|
if (q == end-1)
|
|
++q;
|
|
}
|
|
num = count_lines(text, q);
|
|
if (q == end)
|
|
num++;
|
|
{ // dance around potentially-reallocated text[]
|
|
uintptr_t ofs = q - text;
|
|
size = file_insert(fn, q, 0);
|
|
q = text + ofs;
|
|
}
|
|
if (size < 0)
|
|
goto ret; // nothing was inserted
|
|
// how many lines in text[]?
|
|
li = count_lines(q, q + size - 1);
|
|
status_line("'%s'"
|
|
IF_FEATURE_VI_READONLY("%s")
|
|
" %uL, %uC",
|
|
fn,
|
|
IF_FEATURE_VI_READONLY((readonly_mode ? " [Readonly]" : ""),)
|
|
li, size
|
|
);
|
|
dot = find_line(num);
|
|
} else if (strncmp(cmd, "rewind", i) == 0) { // rewind cmd line args
|
|
if (modified_count && !useforce) {
|
|
status_line_bold("No write since last change (:%s! overrides)", cmd);
|
|
} else {
|
|
// reset the filenames to edit
|
|
optind = -1; // start from 0th file
|
|
editing = 0;
|
|
}
|
|
# if ENABLE_FEATURE_VI_SET
|
|
} else if (strncmp(cmd, "set", i) == 0) { // set or clear features
|
|
# if ENABLE_FEATURE_VI_SETOPTS
|
|
char *argp, *argn, oldch;
|
|
# endif
|
|
// only blank is regarded as args delimiter. What about tab '\t'?
|
|
if (!args[0] || strcmp(args, "all") == 0) {
|
|
// print out values of all options
|
|
# if ENABLE_FEATURE_VI_SETOPTS
|
|
status_line_bold(
|
|
"%sautoindent "
|
|
"%sexpandtab "
|
|
"%sflash "
|
|
"%signorecase "
|
|
"%sshowmatch "
|
|
"tabstop=%u",
|
|
autoindent ? "" : "no",
|
|
expandtab ? "" : "no",
|
|
err_method ? "" : "no",
|
|
ignorecase ? "" : "no",
|
|
showmatch ? "" : "no",
|
|
tabstop
|
|
);
|
|
# endif
|
|
goto ret;
|
|
}
|
|
# if ENABLE_FEATURE_VI_SETOPTS
|
|
argp = args;
|
|
while (*argp) {
|
|
i = 0;
|
|
if (argp[0] == 'n' && argp[1] == 'o') // "noXXX"
|
|
i = 2;
|
|
argn = skip_non_whitespace(argp);
|
|
oldch = *argn;
|
|
*argn = '\0';
|
|
setops(argp, i);
|
|
*argn = oldch;
|
|
argp = skip_whitespace(argn);
|
|
}
|
|
# endif /* FEATURE_VI_SETOPTS */
|
|
# endif /* FEATURE_VI_SET */
|
|
|
|
# if ENABLE_FEATURE_VI_SEARCH
|
|
} else if (cmd[0] == 's') { // substitute a pattern with a replacement pattern
|
|
char *F, *R, *flags;
|
|
size_t len_F, len_R;
|
|
int gflag = 0; // global replace flag
|
|
int subs = 0; // number of substitutions
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
int last_line = 0, lines = 0;
|
|
# endif
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
regex_t preg;
|
|
int cflags;
|
|
char *Rorig;
|
|
# if ENABLE_FEATURE_VI_UNDO
|
|
int undo = 0;
|
|
# endif
|
|
# endif
|
|
|
|
// F points to the "find" pattern
|
|
// R points to the "replace" pattern
|
|
// replace the cmd line delimiters "/" with NULs
|
|
c = buf[1]; // what is the delimiter
|
|
F = buf + 2; // start of "find"
|
|
R = strchr_backslash(F, c); // middle delimiter
|
|
if (!R)
|
|
goto colon_s_fail;
|
|
len_F = R - F;
|
|
*R++ = '\0'; // terminate "find"
|
|
flags = strchr_backslash(R, c);
|
|
if (flags) {
|
|
*flags++ = '\0'; // terminate "replace"
|
|
gflag = *flags;
|
|
}
|
|
|
|
if (len_F) { // save "find" as last search pattern
|
|
free(last_search_pattern);
|
|
last_search_pattern = xstrdup(F - 1);
|
|
last_search_pattern[0] = '/';
|
|
} else if (last_search_pattern[1] == '\0') {
|
|
status_line_bold("No previous search");
|
|
goto ret;
|
|
} else {
|
|
F = last_search_pattern + 1;
|
|
len_F = strlen(F);
|
|
}
|
|
|
|
if (!GOT_ADDRESS) { // no addr given
|
|
q = begin_line(dot); // start with cur line
|
|
r = end_line(dot);
|
|
b = e = count_lines(text, q); // cur line number
|
|
} else if (!GOT_RANGE) { // one addr given
|
|
b = e;
|
|
}
|
|
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
Rorig = R;
|
|
cflags = 0;
|
|
if (ignorecase)
|
|
cflags = REG_ICASE;
|
|
memset(&preg, 0, sizeof(preg));
|
|
if (regcomp(&preg, F, cflags) != 0) {
|
|
status_line(":s bad search pattern");
|
|
goto regex_search_end;
|
|
}
|
|
# else
|
|
len_R = strlen(R);
|
|
# endif
|
|
|
|
for (i = b; i <= e; i++) { // so, :20,23 s \0 find \0 replace \0
|
|
char *ls = q; // orig line start
|
|
char *found;
|
|
vc4:
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
found = regex_search(q, &preg, Rorig, &len_F, &len_R, &R);
|
|
# else
|
|
found = char_search(q, F, (FORWARD << 1) | LIMITED); // search cur line only for "find"
|
|
# endif
|
|
if (found) {
|
|
uintptr_t bias;
|
|
// we found the "find" pattern - delete it
|
|
// For undo support, the first item should not be chained
|
|
// This needs to be handled differently depending on
|
|
// whether or not regex support is enabled.
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
# define TEST_LEN_F len_F // len_F may be zero
|
|
# define TEST_UNDO1 undo++
|
|
# define TEST_UNDO2 undo++
|
|
# else
|
|
# define TEST_LEN_F 1 // len_F is never zero
|
|
# define TEST_UNDO1 subs
|
|
# define TEST_UNDO2 1
|
|
# endif
|
|
if (TEST_LEN_F) // match can be empty, no delete needed
|
|
text_hole_delete(found, found + len_F - 1,
|
|
TEST_UNDO1 ? ALLOW_UNDO_CHAIN : ALLOW_UNDO);
|
|
if (len_R != 0) { // insert the "replace" pattern, if required
|
|
bias = string_insert(found, R,
|
|
TEST_UNDO2 ? ALLOW_UNDO_CHAIN : ALLOW_UNDO);
|
|
found += bias;
|
|
ls += bias;
|
|
//q += bias; - recalculated anyway
|
|
}
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
free(R);
|
|
# endif
|
|
if (TEST_LEN_F || len_R != 0) {
|
|
dot = ls;
|
|
subs++;
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
if (last_line != i) {
|
|
last_line = i;
|
|
++lines;
|
|
}
|
|
# endif
|
|
}
|
|
// check for "global" :s/foo/bar/g
|
|
if (gflag == 'g') {
|
|
if ((found + len_R) < end_line(ls)) {
|
|
q = found + len_R;
|
|
goto vc4; // don't let q move past cur line
|
|
}
|
|
}
|
|
}
|
|
q = next_line(ls);
|
|
}
|
|
if (subs == 0) {
|
|
status_line_bold("No match");
|
|
} else {
|
|
dot_skip_over_ws();
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
if (subs > 1)
|
|
status_line("%d substitutions on %d lines", subs, lines);
|
|
# endif
|
|
}
|
|
# if ENABLE_FEATURE_VI_REGEX_SEARCH
|
|
regex_search_end:
|
|
regfree(&preg);
|
|
# endif
|
|
# endif /* FEATURE_VI_SEARCH */
|
|
} else if (strncmp(cmd, "version", i) == 0) { // show software version
|
|
status_line(BB_VER);
|
|
} else if (strncmp(cmd, "write", i) == 0 // write text to file
|
|
|| strcmp(cmd, "wq") == 0
|
|
|| strcmp(cmd, "wn") == 0
|
|
|| (cmd[0] == 'x' && !cmd[1])
|
|
) {
|
|
int size;
|
|
//int forced = FALSE;
|
|
|
|
// is there a file name to write to?
|
|
if (args[0]) {
|
|
struct stat statbuf;
|
|
|
|
exp = expand_args(args);
|
|
if (exp == NULL)
|
|
goto ret;
|
|
if (!useforce && (fn == NULL || strcmp(fn, exp) != 0) &&
|
|
stat(exp, &statbuf) == 0) {
|
|
status_line_bold("File exists (:w! overrides)");
|
|
goto ret;
|
|
}
|
|
fn = exp;
|
|
init_filename(fn);
|
|
}
|
|
# if ENABLE_FEATURE_VI_READONLY
|
|
else if (readonly_mode && !useforce && fn) {
|
|
status_line_bold("'%s' is read only", fn);
|
|
goto ret;
|
|
}
|
|
# endif
|
|
//if (useforce) {
|
|
// if "fn" is not write-able, chmod u+w
|
|
// sprintf(syscmd, "chmod u+w %s", fn);
|
|
// system(syscmd);
|
|
// forced = TRUE;
|
|
//}
|
|
if (modified_count != 0 || cmd[0] != 'x') {
|
|
size = r - q + 1;
|
|
l = file_write(fn, q, r);
|
|
} else {
|
|
size = 0;
|
|
l = 0;
|
|
}
|
|
//if (useforce && forced) {
|
|
// chmod u-w
|
|
// sprintf(syscmd, "chmod u-w %s", fn);
|
|
// system(syscmd);
|
|
// forced = FALSE;
|
|
//}
|
|
if (l < 0) {
|
|
if (l == -1)
|
|
status_line_bold_errno(fn);
|
|
} else {
|
|
// how many lines written
|
|
li = count_lines(q, q + l - 1);
|
|
status_line("'%s' %uL, %uC", fn, li, l);
|
|
if (l == size) {
|
|
if (q == text && q + l == end) {
|
|
modified_count = 0;
|
|
last_modified_count = -1;
|
|
}
|
|
if (cmd[1] == 'n') {
|
|
editing = 0;
|
|
} else if (cmd[0] == 'x' || cmd[1] == 'q') {
|
|
// are there other files to edit?
|
|
int n = cmdline_filecnt - optind - 1;
|
|
if (n > 0) {
|
|
if (useforce) {
|
|
// force end of argv list
|
|
optind = cmdline_filecnt;
|
|
} else {
|
|
status_line_bold("%u more file(s) to edit", n);
|
|
goto ret;
|
|
}
|
|
}
|
|
editing = 0;
|
|
}
|
|
}
|
|
}
|
|
# if ENABLE_FEATURE_VI_YANKMARK
|
|
} else if (strncmp(cmd, "yank", i) == 0) { // yank lines
|
|
if (!GOT_ADDRESS) { // no addr given- use defaults
|
|
q = begin_line(dot); // assume .,. for the range
|
|
r = end_line(dot);
|
|
}
|
|
text_yank(q, r, YDreg, WHOLE);
|
|
li = count_lines(q, r);
|
|
status_line("Yank %d lines (%d chars) into [%c]",
|
|
li, strlen(reg[YDreg]), what_reg());
|
|
# endif
|
|
} else {
|
|
// cmd unknown
|
|
not_implemented(cmd);
|
|
}
|
|
ret:
|
|
# if ENABLE_FEATURE_VI_COLON_EXPAND
|
|
free(exp);
|
|
# endif
|
|
dot = bound_dot(dot); // make sure "dot" is valid
|
|
return;
|
|
# if ENABLE_FEATURE_VI_SEARCH
|
|
colon_s_fail:
|
|
status_line(":s expression missing delimiters");
|
|
# endif
|
|
#endif /* FEATURE_VI_COLON */
|
|
}
|
|
|
|
//----- Char Routines --------------------------------------------
|
|
// Chars that are part of a word-
|
|
// 0123456789_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
|
|
// Chars that are Not part of a word (stoppers)
|
|
// !"#$%&'()*+,-./:;<=>?@[\]^`{|}~
|
|
// Chars that are WhiteSpace
|
|
// TAB NEWLINE VT FF RETURN SPACE
|
|
// DO NOT COUNT NEWLINE AS WHITESPACE
|
|
|
|
static int st_test(char *p, int type, int dir, char *tested)
|
|
{
|
|
char c, c0, ci;
|
|
int test, inc;
|
|
|
|
inc = dir;
|
|
c = c0 = p[0];
|
|
ci = p[inc];
|
|
test = 0;
|
|
|
|
if (type == S_BEFORE_WS) {
|
|
c = ci;
|
|
test = (!isspace(c) || c == '\n');
|
|
}
|
|
if (type == S_TO_WS) {
|
|
c = c0;
|
|
test = (!isspace(c) || c == '\n');
|
|
}
|
|
if (type == S_OVER_WS) {
|
|
c = c0;
|
|
test = isspace(c);
|
|
}
|
|
if (type == S_END_PUNCT) {
|
|
c = ci;
|
|
test = ispunct(c);
|
|
}
|
|
if (type == S_END_ALNUM) {
|
|
c = ci;
|
|
test = (isalnum(c) || c == '_');
|
|
}
|
|
*tested = c;
|
|
return test;
|
|
}
|
|
|
|
static char *skip_thing(char *p, int linecnt, int dir, int type)
|
|
{
|
|
char c;
|
|
|
|
while (st_test(p, type, dir, &c)) {
|
|
// make sure we limit search to correct number of lines
|
|
if (c == '\n' && --linecnt < 1)
|
|
break;
|
|
if (dir >= 0 && p >= end - 1)
|
|
break;
|
|
if (dir < 0 && p <= text)
|
|
break;
|
|
p += dir; // move to next char
|
|
}
|
|
return p;
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_USE_SIGNALS
|
|
static void winch_handler(int sig UNUSED_PARAM)
|
|
{
|
|
int save_errno = errno;
|
|
// FIXME: do it in main loop!!!
|
|
signal(SIGWINCH, winch_handler);
|
|
query_screen_dimensions();
|
|
new_screen(rows, columns); // get memory for virtual screen
|
|
redraw(TRUE); // re-draw the screen
|
|
errno = save_errno;
|
|
}
|
|
static void tstp_handler(int sig UNUSED_PARAM)
|
|
{
|
|
int save_errno = errno;
|
|
|
|
// ioctl inside cookmode() was seen to generate SIGTTOU,
|
|
// stopping us too early. Prevent that:
|
|
signal(SIGTTOU, SIG_IGN);
|
|
|
|
go_bottom_and_clear_to_eol();
|
|
cookmode(); // terminal to "cooked"
|
|
|
|
// stop now
|
|
//signal(SIGTSTP, SIG_DFL);
|
|
//raise(SIGTSTP);
|
|
raise(SIGSTOP); // avoid "dance" with TSTP handler - use SIGSTOP instead
|
|
//signal(SIGTSTP, tstp_handler);
|
|
|
|
// we have been "continued" with SIGCONT, restore screen and termios
|
|
rawmode(); // terminal to "raw"
|
|
last_status_cksum = 0; // force status update
|
|
redraw(TRUE); // re-draw the screen
|
|
|
|
errno = save_errno;
|
|
}
|
|
static void int_handler(int sig)
|
|
{
|
|
signal(SIGINT, int_handler);
|
|
siglongjmp(restart, sig);
|
|
}
|
|
#endif /* FEATURE_VI_USE_SIGNALS */
|
|
|
|
static void do_cmd(int c);
|
|
|
|
static int at_eof(const char *s)
|
|
{
|
|
// does 's' point to end of file, even with no terminating newline?
|
|
return ((s == end - 2 && s[1] == '\n') || s == end - 1);
|
|
}
|
|
|
|
static int find_range(char **start, char **stop, int cmd)
|
|
{
|
|
char *p, *q, *t;
|
|
int buftype = -1;
|
|
int c;
|
|
|
|
p = q = dot;
|
|
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
if (cmd == 'Y') {
|
|
c = 'y';
|
|
} else
|
|
#endif
|
|
{
|
|
c = get_motion_char();
|
|
}
|
|
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
if ((cmd == 'Y' || cmd == c) && strchr("cdy><", c)) {
|
|
#else
|
|
if (cmd == c && strchr("cd><", c)) {
|
|
#endif
|
|
// these cmds operate on whole lines
|
|
buftype = WHOLE;
|
|
if (--cmdcnt > 0) {
|
|
do_cmd('j');
|
|
if (cmd_error)
|
|
buftype = -1;
|
|
}
|
|
} else if (strchr("^%$0bBeEfFtThnN/?|{}\b\177", c)) {
|
|
// Most operate on char positions within a line. Of those that
|
|
// don't '%' needs no special treatment, search commands are
|
|
// marked as MULTI and "{}" are handled below.
|
|
buftype = strchr("nN/?", c) ? MULTI : PARTIAL;
|
|
do_cmd(c); // execute movement cmd
|
|
if (p == dot) // no movement is an error
|
|
buftype = -1;
|
|
} else if (strchr("wW", c)) {
|
|
buftype = MULTI;
|
|
do_cmd(c); // execute movement cmd
|
|
// step back one char, but not if we're at end of file,
|
|
// or if we are at EOF and search was for 'w' and we're at
|
|
// the start of a 'W' word.
|
|
if (dot > p && (!at_eof(dot) || (c == 'w' && ispunct(*dot))))
|
|
dot--;
|
|
t = dot;
|
|
// don't include trailing WS as part of word
|
|
while (dot > p && isspace(*dot)) {
|
|
if (*dot-- == '\n')
|
|
t = dot;
|
|
}
|
|
// for non-change operations WS after NL is not part of word
|
|
if (cmd != 'c' && dot != t && *dot != '\n')
|
|
dot = t;
|
|
} else if (strchr("GHL+-gjk'\r\n", c)) {
|
|
// these operate on whole lines
|
|
buftype = WHOLE;
|
|
do_cmd(c); // execute movement cmd
|
|
if (cmd_error)
|
|
buftype = -1;
|
|
} else if (c == ' ' || c == 'l') {
|
|
// forward motion by character
|
|
int tmpcnt = (cmdcnt ?: 1);
|
|
buftype = PARTIAL;
|
|
do_cmd(c); // execute movement cmd
|
|
// exclude last char unless range isn't what we expected
|
|
// this indicates we've hit EOL
|
|
if (tmpcnt == dot - p)
|
|
dot--;
|
|
}
|
|
|
|
if (buftype == -1) {
|
|
if (c != 27)
|
|
indicate_error();
|
|
return buftype;
|
|
}
|
|
|
|
q = dot;
|
|
if (q < p) {
|
|
t = q;
|
|
q = p;
|
|
p = t;
|
|
}
|
|
|
|
// movements which don't include end of range
|
|
if (q > p) {
|
|
if (strchr("^0bBFThnN/?|\b\177", c)) {
|
|
q--;
|
|
} else if (strchr("{}", c)) {
|
|
buftype = (p == begin_line(p) && (*q == '\n' || at_eof(q))) ?
|
|
WHOLE : MULTI;
|
|
if (!at_eof(q)) {
|
|
q--;
|
|
if (q > p && p != begin_line(p))
|
|
q--;
|
|
}
|
|
}
|
|
}
|
|
|
|
*start = p;
|
|
*stop = q;
|
|
return buftype;
|
|
}
|
|
|
|
//---------------------------------------------------------------------
|
|
//----- the Ascii Chart -----------------------------------------------
|
|
// 00 nul 01 soh 02 stx 03 etx 04 eot 05 enq 06 ack 07 bel
|
|
// 08 bs 09 ht 0a nl 0b vt 0c np 0d cr 0e so 0f si
|
|
// 10 dle 11 dc1 12 dc2 13 dc3 14 dc4 15 nak 16 syn 17 etb
|
|
// 18 can 19 em 1a sub 1b esc 1c fs 1d gs 1e rs 1f us
|
|
// 20 sp 21 ! 22 " 23 # 24 $ 25 % 26 & 27 '
|
|
// 28 ( 29 ) 2a * 2b + 2c , 2d - 2e . 2f /
|
|
// 30 0 31 1 32 2 33 3 34 4 35 5 36 6 37 7
|
|
// 38 8 39 9 3a : 3b ; 3c < 3d = 3e > 3f ?
|
|
// 40 @ 41 A 42 B 43 C 44 D 45 E 46 F 47 G
|
|
// 48 H 49 I 4a J 4b K 4c L 4d M 4e N 4f O
|
|
// 50 P 51 Q 52 R 53 S 54 T 55 U 56 V 57 W
|
|
// 58 X 59 Y 5a Z 5b [ 5c \ 5d ] 5e ^ 5f _
|
|
// 60 ` 61 a 62 b 63 c 64 d 65 e 66 f 67 g
|
|
// 68 h 69 i 6a j 6b k 6c l 6d m 6e n 6f o
|
|
// 70 p 71 q 72 r 73 s 74 t 75 u 76 v 77 w
|
|
// 78 x 79 y 7a z 7b { 7c | 7d } 7e ~ 7f del
|
|
//---------------------------------------------------------------------
|
|
|
|
//----- Execute a Vi Command -----------------------------------
|
|
static void do_cmd(int c)
|
|
{
|
|
char *p, *q, *save_dot;
|
|
char buf[12];
|
|
int dir;
|
|
int cnt, i, j;
|
|
int c1;
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
char *orig_dot = dot;
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
int allow_undo = ALLOW_UNDO;
|
|
int undo_del = UNDO_DEL;
|
|
#endif
|
|
|
|
// c1 = c; // quiet the compiler
|
|
// cnt = yf = 0; // quiet the compiler
|
|
// p = q = save_dot = buf; // quiet the compiler
|
|
memset(buf, '\0', sizeof(buf));
|
|
keep_index = FALSE;
|
|
cmd_error = FALSE;
|
|
|
|
show_status_line();
|
|
|
|
// if this is a cursor key, skip these checks
|
|
switch (c) {
|
|
case KEYCODE_UP:
|
|
case KEYCODE_DOWN:
|
|
case KEYCODE_LEFT:
|
|
case KEYCODE_RIGHT:
|
|
case KEYCODE_HOME:
|
|
case KEYCODE_END:
|
|
case KEYCODE_PAGEUP:
|
|
case KEYCODE_PAGEDOWN:
|
|
case KEYCODE_DELETE:
|
|
goto key_cmd_mode;
|
|
}
|
|
|
|
if (cmd_mode == 2) {
|
|
// flip-flop Insert/Replace mode
|
|
if (c == KEYCODE_INSERT)
|
|
goto dc_i;
|
|
// we are 'R'eplacing the current *dot with new char
|
|
if (*dot == '\n') {
|
|
// don't Replace past E-o-l
|
|
cmd_mode = 1; // convert to insert
|
|
undo_queue_commit();
|
|
} else {
|
|
if (1 <= c || Isprint(c)) {
|
|
if (c != 27 && !isbackspace(c))
|
|
dot = yank_delete(dot, dot, PARTIAL, YANKDEL, ALLOW_UNDO);
|
|
dot = char_insert(dot, c, ALLOW_UNDO_CHAIN);
|
|
}
|
|
goto dc1;
|
|
}
|
|
}
|
|
if (cmd_mode == 1) {
|
|
// hitting "Insert" twice means "R" replace mode
|
|
if (c == KEYCODE_INSERT) goto dc5;
|
|
// insert the char c at "dot"
|
|
if (1 <= c || Isprint(c)) {
|
|
dot = char_insert(dot, c, ALLOW_UNDO_QUEUED);
|
|
}
|
|
goto dc1;
|
|
}
|
|
|
|
key_cmd_mode:
|
|
switch (c) {
|
|
//case 0x01: // soh
|
|
//case 0x09: // ht
|
|
//case 0x0b: // vt
|
|
//case 0x0e: // so
|
|
//case 0x0f: // si
|
|
//case 0x10: // dle
|
|
//case 0x11: // dc1
|
|
//case 0x13: // dc3
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
case 0x14: // dc4 ctrl-T
|
|
crashme = (crashme == 0) ? 1 : 0;
|
|
break;
|
|
#endif
|
|
//case 0x16: // syn
|
|
//case 0x17: // etb
|
|
//case 0x18: // can
|
|
//case 0x1c: // fs
|
|
//case 0x1d: // gs
|
|
//case 0x1e: // rs
|
|
//case 0x1f: // us
|
|
//case '!': // !-
|
|
//case '#': // #-
|
|
//case '&': // &-
|
|
//case '(': // (-
|
|
//case ')': // )-
|
|
//case '*': // *-
|
|
//case '=': // =-
|
|
//case '@': // @-
|
|
//case 'K': // K-
|
|
//case 'Q': // Q-
|
|
//case 'S': // S-
|
|
//case 'V': // V-
|
|
//case '[': // [-
|
|
//case '\\': // \-
|
|
//case ']': // ]-
|
|
//case '_': // _-
|
|
//case '`': // `-
|
|
//case 'v': // v-
|
|
default: // unrecognized command
|
|
buf[0] = c;
|
|
buf[1] = '\0';
|
|
not_implemented(buf);
|
|
end_cmd_q(); // stop adding to q
|
|
case 0x00: // nul- ignore
|
|
break;
|
|
case 2: // ctrl-B scroll up full screen
|
|
case KEYCODE_PAGEUP: // Cursor Key Page Up
|
|
dot_scroll(rows - 2, -1);
|
|
break;
|
|
case 4: // ctrl-D scroll down half screen
|
|
dot_scroll((rows - 2) / 2, 1);
|
|
break;
|
|
case 5: // ctrl-E scroll down one line
|
|
dot_scroll(1, 1);
|
|
break;
|
|
case 6: // ctrl-F scroll down full screen
|
|
case KEYCODE_PAGEDOWN: // Cursor Key Page Down
|
|
dot_scroll(rows - 2, 1);
|
|
break;
|
|
case 7: // ctrl-G show current status
|
|
last_status_cksum = 0; // force status update
|
|
break;
|
|
case 'h': // h- move left
|
|
case KEYCODE_LEFT: // cursor key Left
|
|
case 8: // ctrl-H- move left (This may be ERASE char)
|
|
case 0x7f: // DEL- move left (This may be ERASE char)
|
|
do {
|
|
dot_left();
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
case 10: // Newline ^J
|
|
case 'j': // j- goto next line, same col
|
|
case KEYCODE_DOWN: // cursor key Down
|
|
case 13: // Carriage Return ^M
|
|
case '+': // +- goto next line
|
|
q = dot;
|
|
do {
|
|
p = next_line(q);
|
|
if (p == end_line(q)) {
|
|
indicate_error();
|
|
goto dc1;
|
|
}
|
|
q = p;
|
|
} while (--cmdcnt > 0);
|
|
dot = q;
|
|
if (c == 13 || c == '+') {
|
|
dot_skip_over_ws();
|
|
} else {
|
|
// try to stay in saved column
|
|
dot = cindex == C_END ? end_line(dot) : move_to_col(dot, cindex);
|
|
keep_index = TRUE;
|
|
}
|
|
break;
|
|
case 12: // ctrl-L force redraw whole screen
|
|
case 18: // ctrl-R force redraw
|
|
redraw(TRUE); // this will redraw the entire display
|
|
break;
|
|
case 21: // ctrl-U scroll up half screen
|
|
dot_scroll((rows - 2) / 2, -1);
|
|
break;
|
|
case 25: // ctrl-Y scroll up one line
|
|
dot_scroll(1, -1);
|
|
break;
|
|
case 27: // esc
|
|
if (cmd_mode == 0)
|
|
indicate_error();
|
|
cmd_mode = 0; // stop inserting
|
|
undo_queue_commit();
|
|
end_cmd_q();
|
|
last_status_cksum = 0; // force status update
|
|
break;
|
|
case ' ': // move right
|
|
case 'l': // move right
|
|
case KEYCODE_RIGHT: // Cursor Key Right
|
|
do {
|
|
dot_right();
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
case '"': // "- name a register to use for Delete/Yank
|
|
c1 = (get_one_char() | 0x20) - 'a'; // | 0x20 is tolower()
|
|
if ((unsigned)c1 <= 25) { // a-z?
|
|
YDreg = c1;
|
|
} else {
|
|
indicate_error();
|
|
}
|
|
break;
|
|
case '\'': // '- goto a specific mark
|
|
c1 = (get_one_char() | 0x20);
|
|
if ((unsigned)(c1 - 'a') <= 25) { // a-z?
|
|
c1 = (c1 - 'a');
|
|
// get the b-o-l
|
|
q = mark[c1];
|
|
if (text <= q && q < end) {
|
|
dot = q;
|
|
dot_begin(); // go to B-o-l
|
|
dot_skip_over_ws();
|
|
} else {
|
|
indicate_error();
|
|
}
|
|
} else if (c1 == '\'') { // goto previous context
|
|
dot = swap_context(dot); // swap current and previous context
|
|
dot_begin(); // go to B-o-l
|
|
dot_skip_over_ws();
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
orig_dot = dot; // this doesn't update stored contexts
|
|
#endif
|
|
} else {
|
|
indicate_error();
|
|
}
|
|
break;
|
|
case 'm': // m- Mark a line
|
|
// this is really stupid. If there are any inserts or deletes
|
|
// between text[0] and dot then this mark will not point to the
|
|
// correct location! It could be off by many lines!
|
|
// Well..., at least its quick and dirty.
|
|
c1 = (get_one_char() | 0x20) - 'a';
|
|
if ((unsigned)c1 <= 25) { // a-z?
|
|
// remember the line
|
|
mark[c1] = dot;
|
|
} else {
|
|
indicate_error();
|
|
}
|
|
break;
|
|
case 'P': // P- Put register before
|
|
case 'p': // p- put register after
|
|
p = reg[YDreg];
|
|
if (p == NULL) {
|
|
status_line_bold("Nothing in register %c", what_reg());
|
|
break;
|
|
}
|
|
cnt = 0;
|
|
i = cmdcnt ?: 1;
|
|
// are we putting whole lines or strings
|
|
if (regtype[YDreg] == WHOLE) {
|
|
if (c == 'P') {
|
|
dot_begin(); // putting lines- Put above
|
|
}
|
|
else /* if ( c == 'p') */ {
|
|
// are we putting after very last line?
|
|
if (end_line(dot) == (end - 1)) {
|
|
dot = end; // force dot to end of text[]
|
|
} else {
|
|
dot_next(); // next line, then put before
|
|
}
|
|
}
|
|
} else {
|
|
if (c == 'p')
|
|
dot_right(); // move to right, can move to NL
|
|
// how far to move cursor if register doesn't have a NL
|
|
if (strchr(p, '\n') == NULL)
|
|
cnt = i * strlen(p) - 1;
|
|
}
|
|
do {
|
|
// dot is adjusted if text[] is reallocated so we don't have to
|
|
string_insert(dot, p, allow_undo); // insert the string
|
|
# if ENABLE_FEATURE_VI_UNDO
|
|
allow_undo = ALLOW_UNDO_CHAIN;
|
|
# endif
|
|
} while (--cmdcnt > 0);
|
|
dot += cnt;
|
|
dot_skip_over_ws();
|
|
# if ENABLE_FEATURE_VI_YANKMARK && ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
yank_status("Put", p, i);
|
|
# endif
|
|
end_cmd_q(); // stop adding to q
|
|
break;
|
|
case 'U': // U- Undo; replace current line with original version
|
|
if (reg[Ureg] != NULL) {
|
|
p = begin_line(dot);
|
|
q = end_line(dot);
|
|
p = text_hole_delete(p, q, ALLOW_UNDO); // delete cur line
|
|
p += string_insert(p, reg[Ureg], ALLOW_UNDO_CHAIN); // insert orig line
|
|
dot = p;
|
|
dot_skip_over_ws();
|
|
# if ENABLE_FEATURE_VI_YANKMARK && ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
yank_status("Undo", reg[Ureg], 1);
|
|
# endif
|
|
}
|
|
break;
|
|
#endif /* FEATURE_VI_YANKMARK */
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
case 'u': // u- undo last operation
|
|
undo_pop();
|
|
break;
|
|
#endif
|
|
case '$': // $- goto end of line
|
|
case KEYCODE_END: // Cursor Key End
|
|
for (;;) {
|
|
dot = end_line(dot);
|
|
if (--cmdcnt <= 0)
|
|
break;
|
|
dot_next();
|
|
}
|
|
cindex = C_END;
|
|
keep_index = TRUE;
|
|
break;
|
|
case '%': // %- find matching char of pair () [] {}
|
|
for (q = dot; q < end && *q != '\n'; q++) {
|
|
if (strchr("()[]{}", *q) != NULL) {
|
|
// we found half of a pair
|
|
p = find_pair(q, *q);
|
|
if (p == NULL) {
|
|
indicate_error();
|
|
} else {
|
|
dot = p;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (*q == '\n')
|
|
indicate_error();
|
|
break;
|
|
case 'f': // f- forward to a user specified char
|
|
case 'F': // F- backward to a user specified char
|
|
case 't': // t- move to char prior to next x
|
|
case 'T': // T- move to char after previous x
|
|
last_search_char = get_one_char(); // get the search char
|
|
last_search_cmd = c;
|
|
// fall through
|
|
case ';': // ;- look at rest of line for last search char
|
|
case ',': // ,- repeat latest search in opposite direction
|
|
dot_to_char(c != ',' ? last_search_cmd : last_search_cmd ^ 0x20);
|
|
break;
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
case '.': // .- repeat the last modifying command
|
|
// Stuff the last_modifying_cmd back into stdin
|
|
// and let it be re-executed.
|
|
if (lmc_len != 0) {
|
|
if (cmdcnt) // update saved count if current count is non-zero
|
|
dotcnt = cmdcnt;
|
|
last_modifying_cmd[lmc_len] = '\0';
|
|
ioq = ioq_start = xasprintf("%u%s", dotcnt, last_modifying_cmd);
|
|
}
|
|
break;
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_SEARCH
|
|
case 'N': // N- backward search for last pattern
|
|
dir = last_search_pattern[0] == '/' ? BACK : FORWARD;
|
|
goto dc4; // now search for pattern
|
|
break;
|
|
case '?': // ?- backward search for a pattern
|
|
case '/': // /- forward search for a pattern
|
|
buf[0] = c;
|
|
buf[1] = '\0';
|
|
q = get_input_line(buf); // get input line- use "status line"
|
|
if (!q[0]) // user changed mind and erased the "/"- do nothing
|
|
break;
|
|
if (!q[1]) { // if no pat re-use old pat
|
|
if (last_search_pattern[0])
|
|
last_search_pattern[0] = c;
|
|
} else { // strlen(q) > 1: new pat- save it and find
|
|
free(last_search_pattern);
|
|
last_search_pattern = xstrdup(q);
|
|
}
|
|
// fall through
|
|
case 'n': // n- repeat search for last pattern
|
|
// search rest of text[] starting at next char
|
|
// if search fails "dot" is unchanged
|
|
dir = last_search_pattern[0] == '/' ? FORWARD : BACK;
|
|
dc4:
|
|
if (last_search_pattern[1] == '\0') {
|
|
status_line_bold("No previous search");
|
|
break;
|
|
}
|
|
do {
|
|
q = char_search(dot + dir, last_search_pattern + 1,
|
|
(dir << 1) | FULL);
|
|
if (q != NULL) {
|
|
dot = q; // good search, update "dot"
|
|
} else {
|
|
// no pattern found between "dot" and top/bottom of file
|
|
// continue from other end of file
|
|
const char *msg;
|
|
q = char_search(dir == FORWARD ? text : end - 1,
|
|
last_search_pattern + 1, (dir << 1) | FULL);
|
|
if (q != NULL) { // found something
|
|
dot = q; // found new pattern- goto it
|
|
msg = "search hit %s, continuing at %s";
|
|
} else { // pattern is nowhere in file
|
|
cmdcnt = 0; // force exit from loop
|
|
msg = "Pattern not found";
|
|
}
|
|
if (dir == FORWARD)
|
|
status_line_bold(msg, "BOTTOM", "TOP");
|
|
else
|
|
status_line_bold(msg, "TOP", "BOTTOM");
|
|
}
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
case '{': // {- move backward paragraph
|
|
case '}': // }- move forward paragraph
|
|
dir = c == '}' ? FORWARD : BACK;
|
|
do {
|
|
int skip = TRUE; // initially skip consecutive empty lines
|
|
while (dir == FORWARD ? dot < end - 1 : dot > text) {
|
|
if (*dot == '\n' && dot[dir] == '\n') {
|
|
if (!skip) {
|
|
if (dir == FORWARD)
|
|
++dot; // move to next blank line
|
|
goto dc2;
|
|
}
|
|
}
|
|
else {
|
|
skip = FALSE;
|
|
}
|
|
dot += dir;
|
|
}
|
|
goto dc6; // end of file
|
|
dc2: continue;
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
#endif /* FEATURE_VI_SEARCH */
|
|
case '0': // 0- goto beginning of line
|
|
case '1': // 1-
|
|
case '2': // 2-
|
|
case '3': // 3-
|
|
case '4': // 4-
|
|
case '5': // 5-
|
|
case '6': // 6-
|
|
case '7': // 7-
|
|
case '8': // 8-
|
|
case '9': // 9-
|
|
if (c == '0' && cmdcnt < 1) {
|
|
dot_begin(); // this was a standalone zero
|
|
} else {
|
|
cmdcnt = cmdcnt * 10 + (c - '0'); // this 0 is part of a number
|
|
}
|
|
break;
|
|
case ':': // :- the colon mode commands
|
|
p = get_input_line(":"); // get input line- use "status line"
|
|
colon(p); // execute the command
|
|
break;
|
|
case '<': // <- Left shift something
|
|
case '>': // >- Right shift something
|
|
cnt = count_lines(text, dot); // remember what line we are on
|
|
if (find_range(&p, &q, c) == -1)
|
|
goto dc6;
|
|
i = count_lines(p, q); // # of lines we are shifting
|
|
for (p = begin_line(p); i > 0; i--, p = next_line(p)) {
|
|
if (c == '<') {
|
|
// shift left- remove tab or tabstop spaces
|
|
if (*p == '\t') {
|
|
// shrink buffer 1 char
|
|
text_hole_delete(p, p, allow_undo);
|
|
} else if (*p == ' ') {
|
|
// we should be calculating columns, not just SPACE
|
|
for (j = 0; *p == ' ' && j < tabstop; j++) {
|
|
text_hole_delete(p, p, allow_undo);
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
allow_undo = ALLOW_UNDO_CHAIN;
|
|
#endif
|
|
}
|
|
}
|
|
} else if (/* c == '>' && */ p != end_line(p)) {
|
|
// shift right -- add tab or tabstop spaces on non-empty lines
|
|
char_insert(p, '\t', allow_undo);
|
|
}
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
allow_undo = ALLOW_UNDO_CHAIN;
|
|
#endif
|
|
}
|
|
dot = find_line(cnt); // what line were we on
|
|
dot_skip_over_ws();
|
|
end_cmd_q(); // stop adding to q
|
|
break;
|
|
case 'A': // A- append at e-o-l
|
|
dot_end(); // go to e-o-l
|
|
//**** fall through to ... 'a'
|
|
case 'a': // a- append after current char
|
|
if (*dot != '\n')
|
|
dot++;
|
|
goto dc_i;
|
|
break;
|
|
case 'B': // B- back a blank-delimited Word
|
|
case 'E': // E- end of a blank-delimited word
|
|
case 'W': // W- forward a blank-delimited word
|
|
dir = FORWARD;
|
|
if (c == 'B')
|
|
dir = BACK;
|
|
do {
|
|
if (c == 'W' || isspace(dot[dir])) {
|
|
dot = skip_thing(dot, 1, dir, S_TO_WS);
|
|
dot = skip_thing(dot, 2, dir, S_OVER_WS);
|
|
}
|
|
if (c != 'W')
|
|
dot = skip_thing(dot, 1, dir, S_BEFORE_WS);
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
case 'C': // C- Change to e-o-l
|
|
case 'D': // D- delete to e-o-l
|
|
save_dot = dot;
|
|
dot = dollar_line(dot); // move to before NL
|
|
// copy text into a register and delete
|
|
dot = yank_delete(save_dot, dot, PARTIAL, YANKDEL, ALLOW_UNDO); // delete to e-o-l
|
|
if (c == 'C')
|
|
goto dc_i; // start inserting
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
if (c == 'D')
|
|
end_cmd_q(); // stop adding to q
|
|
#endif
|
|
break;
|
|
case 'g': // 'gg' goto a line number (vim) (default: very first line)
|
|
c1 = get_one_char();
|
|
if (c1 != 'g') {
|
|
buf[0] = 'g';
|
|
// c1 < 0 if the key was special. Try "g<up-arrow>"
|
|
// TODO: if Unicode?
|
|
buf[1] = (c1 >= 0 ? c1 : '*');
|
|
buf[2] = '\0';
|
|
not_implemented(buf);
|
|
cmd_error = TRUE;
|
|
break;
|
|
}
|
|
if (cmdcnt == 0)
|
|
cmdcnt = 1;
|
|
// fall through
|
|
case 'G': // G- goto to a line number (default= E-O-F)
|
|
dot = end - 1; // assume E-O-F
|
|
if (cmdcnt > 0) {
|
|
dot = find_line(cmdcnt); // what line is #cmdcnt
|
|
}
|
|
dot_begin();
|
|
dot_skip_over_ws();
|
|
break;
|
|
case 'H': // H- goto top line on screen
|
|
dot = screenbegin;
|
|
if (cmdcnt > (rows - 1)) {
|
|
cmdcnt = (rows - 1);
|
|
}
|
|
while (--cmdcnt > 0) {
|
|
dot_next();
|
|
}
|
|
dot_begin();
|
|
dot_skip_over_ws();
|
|
break;
|
|
case 'I': // I- insert before first non-blank
|
|
dot_begin(); // 0
|
|
dot_skip_over_ws();
|
|
//**** fall through to ... 'i'
|
|
case 'i': // i- insert before current char
|
|
case KEYCODE_INSERT: // Cursor Key Insert
|
|
dc_i:
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
newindent = -1;
|
|
#endif
|
|
cmd_mode = 1; // start inserting
|
|
undo_queue_commit(); // commit queue when cmd_mode changes
|
|
break;
|
|
case 'J': // J- join current and next lines together
|
|
do {
|
|
dot_end(); // move to NL
|
|
if (dot < end - 1) { // make sure not last char in text[]
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
undo_push(dot, 1, UNDO_DEL);
|
|
*dot++ = ' '; // replace NL with space
|
|
undo_push((dot - 1), 1, UNDO_INS_CHAIN);
|
|
#else
|
|
*dot++ = ' ';
|
|
modified_count++;
|
|
#endif
|
|
while (isblank(*dot)) { // delete leading WS
|
|
text_hole_delete(dot, dot, ALLOW_UNDO_CHAIN);
|
|
}
|
|
}
|
|
} while (--cmdcnt > 0);
|
|
end_cmd_q(); // stop adding to q
|
|
break;
|
|
case 'L': // L- goto bottom line on screen
|
|
dot = end_screen();
|
|
if (cmdcnt > (rows - 1)) {
|
|
cmdcnt = (rows - 1);
|
|
}
|
|
while (--cmdcnt > 0) {
|
|
dot_prev();
|
|
}
|
|
dot_begin();
|
|
dot_skip_over_ws();
|
|
break;
|
|
case 'M': // M- goto middle line on screen
|
|
dot = screenbegin;
|
|
for (cnt = 0; cnt < (rows-1) / 2; cnt++)
|
|
dot = next_line(dot);
|
|
dot_skip_over_ws();
|
|
break;
|
|
case 'O': // O- open an empty line above
|
|
dot_begin();
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
// special case: use indent of current line
|
|
newindent = get_column(dot + indent_len(dot));
|
|
#endif
|
|
goto dc3;
|
|
case 'o': // o- open an empty line below
|
|
dot_end();
|
|
dc3:
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
cmd_mode = 1; // switch to insert mode early
|
|
#endif
|
|
dot = char_insert(dot, '\n', ALLOW_UNDO);
|
|
if (c == 'O' && !autoindent) {
|
|
// done in char_insert() for 'O'+autoindent
|
|
dot_prev();
|
|
}
|
|
goto dc_i;
|
|
break;
|
|
case 'R': // R- continuous Replace char
|
|
dc5:
|
|
cmd_mode = 2;
|
|
undo_queue_commit();
|
|
rstart = dot;
|
|
break;
|
|
case KEYCODE_DELETE:
|
|
if (dot < end - 1)
|
|
dot = yank_delete(dot, dot, PARTIAL, YANKDEL, ALLOW_UNDO);
|
|
break;
|
|
case 'X': // X- delete char before dot
|
|
case 'x': // x- delete the current char
|
|
case 's': // s- substitute the current char
|
|
dir = 0;
|
|
if (c == 'X')
|
|
dir = -1;
|
|
do {
|
|
if (dot[dir] != '\n') {
|
|
if (c == 'X')
|
|
dot--; // delete prev char
|
|
dot = yank_delete(dot, dot, PARTIAL, YANKDEL, allow_undo); // delete char
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
allow_undo = ALLOW_UNDO_CHAIN;
|
|
#endif
|
|
}
|
|
} while (--cmdcnt > 0);
|
|
end_cmd_q(); // stop adding to q
|
|
if (c == 's')
|
|
goto dc_i; // start inserting
|
|
break;
|
|
case 'Z': // Z- if modified, {write}; exit
|
|
// ZZ means to save file (if necessary), then exit
|
|
c1 = get_one_char();
|
|
if (c1 != 'Z') {
|
|
indicate_error();
|
|
break;
|
|
}
|
|
if (modified_count) {
|
|
if (ENABLE_FEATURE_VI_READONLY && readonly_mode && current_filename) {
|
|
status_line_bold("'%s' is read only", current_filename);
|
|
break;
|
|
}
|
|
cnt = file_write(current_filename, text, end - 1);
|
|
if (cnt < 0) {
|
|
if (cnt == -1)
|
|
status_line_bold("Write error: "STRERROR_FMT STRERROR_ERRNO);
|
|
} else if (cnt == (end - 1 - text + 1)) {
|
|
editing = 0;
|
|
}
|
|
} else {
|
|
editing = 0;
|
|
}
|
|
// are there other files to edit?
|
|
j = cmdline_filecnt - optind - 1;
|
|
if (editing == 0 && j > 0) {
|
|
editing = 1;
|
|
modified_count = 0;
|
|
last_modified_count = -1;
|
|
status_line_bold("%u more file(s) to edit", j);
|
|
}
|
|
break;
|
|
case '^': // ^- move to first non-blank on line
|
|
dot_begin();
|
|
dot_skip_over_ws();
|
|
break;
|
|
case 'b': // b- back a word
|
|
case 'e': // e- end of word
|
|
dir = FORWARD;
|
|
if (c == 'b')
|
|
dir = BACK;
|
|
do {
|
|
if ((dot + dir) < text || (dot + dir) > end - 1)
|
|
break;
|
|
dot += dir;
|
|
if (isspace(*dot)) {
|
|
dot = skip_thing(dot, (c == 'e') ? 2 : 1, dir, S_OVER_WS);
|
|
}
|
|
if (isalnum(*dot) || *dot == '_') {
|
|
dot = skip_thing(dot, 1, dir, S_END_ALNUM);
|
|
} else if (ispunct(*dot)) {
|
|
dot = skip_thing(dot, 1, dir, S_END_PUNCT);
|
|
}
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
case 'c': // c- change something
|
|
case 'd': // d- delete something
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
case 'y': // y- yank something
|
|
case 'Y': // Y- Yank a line
|
|
#endif
|
|
{
|
|
int yf = YANKDEL; // assume either "c" or "d"
|
|
int buftype;
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
# if ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
char *savereg = reg[YDreg];
|
|
# endif
|
|
if (c == 'y' || c == 'Y')
|
|
yf = YANKONLY;
|
|
#endif
|
|
// determine range, and whether it spans lines
|
|
buftype = find_range(&p, &q, c);
|
|
if (buftype == -1) // invalid range
|
|
goto dc6;
|
|
if (buftype == WHOLE) {
|
|
save_dot = p; // final cursor position is start of range
|
|
p = begin_line(p);
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
if (c == 'c') // special case: use indent of current line
|
|
newindent = get_column(p + indent_len(p));
|
|
#endif
|
|
q = end_line(q);
|
|
}
|
|
dot = yank_delete(p, q, buftype, yf, ALLOW_UNDO); // delete word
|
|
if (buftype == WHOLE) {
|
|
if (c == 'c') {
|
|
#if ENABLE_FEATURE_VI_SETOPTS
|
|
cmd_mode = 1; // switch to insert mode early
|
|
#endif
|
|
dot = char_insert(dot, '\n', ALLOW_UNDO_CHAIN);
|
|
// on the last line of file don't move to prev line,
|
|
// handled in char_insert() if autoindent is enabled
|
|
if (dot != (end-1) && !autoindent) {
|
|
dot_prev();
|
|
}
|
|
} else if (c == 'd') {
|
|
dot_begin();
|
|
dot_skip_over_ws();
|
|
} else {
|
|
dot = save_dot;
|
|
}
|
|
}
|
|
// if CHANGING, not deleting, start inserting after the delete
|
|
if (c == 'c') {
|
|
goto dc_i; // start inserting
|
|
}
|
|
#if ENABLE_FEATURE_VI_YANKMARK && ENABLE_FEATURE_VI_VERBOSE_STATUS
|
|
// only update status if a yank has actually happened
|
|
if (reg[YDreg] != savereg)
|
|
yank_status(c == 'd' ? "Delete" : "Yank", reg[YDreg], 1);
|
|
#endif
|
|
dc6:
|
|
end_cmd_q(); // stop adding to q
|
|
break;
|
|
}
|
|
case 'k': // k- goto prev line, same col
|
|
case KEYCODE_UP: // cursor key Up
|
|
case '-': // -- goto prev line
|
|
q = dot;
|
|
do {
|
|
p = prev_line(q);
|
|
if (p == begin_line(q)) {
|
|
indicate_error();
|
|
goto dc1;
|
|
}
|
|
q = p;
|
|
} while (--cmdcnt > 0);
|
|
dot = q;
|
|
if (c == '-') {
|
|
dot_skip_over_ws();
|
|
} else {
|
|
// try to stay in saved column
|
|
dot = cindex == C_END ? end_line(dot) : move_to_col(dot, cindex);
|
|
keep_index = TRUE;
|
|
}
|
|
break;
|
|
case 'r': // r- replace the current char with user input
|
|
c1 = get_one_char(); // get the replacement char
|
|
if (c1 != 27) {
|
|
if (end_line(dot) - dot < (cmdcnt ?: 1)) {
|
|
indicate_error();
|
|
goto dc6;
|
|
}
|
|
do {
|
|
dot = text_hole_delete(dot, dot, allow_undo);
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
allow_undo = ALLOW_UNDO_CHAIN;
|
|
#endif
|
|
dot = char_insert(dot, c1, allow_undo);
|
|
} while (--cmdcnt > 0);
|
|
dot_left();
|
|
}
|
|
end_cmd_q(); // stop adding to q
|
|
break;
|
|
case 'w': // w- forward a word
|
|
do {
|
|
if (isalnum(*dot) || *dot == '_') { // we are on ALNUM
|
|
dot = skip_thing(dot, 1, FORWARD, S_END_ALNUM);
|
|
} else if (ispunct(*dot)) { // we are on PUNCT
|
|
dot = skip_thing(dot, 1, FORWARD, S_END_PUNCT);
|
|
}
|
|
if (dot < end - 1)
|
|
dot++; // move over word
|
|
if (isspace(*dot)) {
|
|
dot = skip_thing(dot, 2, FORWARD, S_OVER_WS);
|
|
}
|
|
} while (--cmdcnt > 0);
|
|
break;
|
|
case 'z': // z-
|
|
c1 = get_one_char(); // get the replacement char
|
|
cnt = 0;
|
|
if (c1 == '.')
|
|
cnt = (rows - 2) / 2; // put dot at center
|
|
if (c1 == '-')
|
|
cnt = rows - 2; // put dot at bottom
|
|
screenbegin = begin_line(dot); // start dot at top
|
|
dot_scroll(cnt, -1);
|
|
break;
|
|
case '|': // |- move to column "cmdcnt"
|
|
dot = move_to_col(dot, cmdcnt - 1); // try to move to column
|
|
break;
|
|
case '~': // ~- flip the case of letters a-z -> A-Z
|
|
do {
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
if (isalpha(*dot)) {
|
|
undo_push(dot, 1, undo_del);
|
|
*dot = islower(*dot) ? toupper(*dot) : tolower(*dot);
|
|
undo_push(dot, 1, UNDO_INS_CHAIN);
|
|
undo_del = UNDO_DEL_CHAIN;
|
|
}
|
|
#else
|
|
if (islower(*dot)) {
|
|
*dot = toupper(*dot);
|
|
modified_count++;
|
|
} else if (isupper(*dot)) {
|
|
*dot = tolower(*dot);
|
|
modified_count++;
|
|
}
|
|
#endif
|
|
dot_right();
|
|
} while (--cmdcnt > 0);
|
|
end_cmd_q(); // stop adding to q
|
|
break;
|
|
//----- The Cursor and Function Keys -----------------------------
|
|
case KEYCODE_HOME: // Cursor Key Home
|
|
dot_begin();
|
|
break;
|
|
// The Fn keys could point to do_macro which could translate them
|
|
#if 0
|
|
case KEYCODE_FUN1: // Function Key F1
|
|
case KEYCODE_FUN2: // Function Key F2
|
|
case KEYCODE_FUN3: // Function Key F3
|
|
case KEYCODE_FUN4: // Function Key F4
|
|
case KEYCODE_FUN5: // Function Key F5
|
|
case KEYCODE_FUN6: // Function Key F6
|
|
case KEYCODE_FUN7: // Function Key F7
|
|
case KEYCODE_FUN8: // Function Key F8
|
|
case KEYCODE_FUN9: // Function Key F9
|
|
case KEYCODE_FUN10: // Function Key F10
|
|
case KEYCODE_FUN11: // Function Key F11
|
|
case KEYCODE_FUN12: // Function Key F12
|
|
break;
|
|
#endif
|
|
}
|
|
|
|
dc1:
|
|
// if text[] just became empty, add back an empty line
|
|
if (end == text) {
|
|
char_insert(text, '\n', NO_UNDO); // start empty buf with dummy line
|
|
dot = text;
|
|
}
|
|
// it is OK for dot to exactly equal to end, otherwise check dot validity
|
|
if (dot != end) {
|
|
dot = bound_dot(dot); // make sure "dot" is valid
|
|
}
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
if (dot != orig_dot)
|
|
check_context(c); // update the current context
|
|
#endif
|
|
|
|
if (!isdigit(c))
|
|
cmdcnt = 0; // cmd was not a number, reset cmdcnt
|
|
cnt = dot - begin_line(dot);
|
|
// Try to stay off of the Newline
|
|
if (*dot == '\n' && cnt > 0 && cmd_mode == 0)
|
|
dot--;
|
|
}
|
|
|
|
// NB! the CRASHME code is unmaintained, and doesn't currently build
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
static int totalcmds = 0;
|
|
static int Mp = 85; // Movement command Probability
|
|
static int Np = 90; // Non-movement command Probability
|
|
static int Dp = 96; // Delete command Probability
|
|
static int Ip = 97; // Insert command Probability
|
|
static int Yp = 98; // Yank command Probability
|
|
static int Pp = 99; // Put command Probability
|
|
static int M = 0, N = 0, I = 0, D = 0, Y = 0, P = 0, U = 0;
|
|
static const char chars[20] = "\t012345 abcdABCD-=.$";
|
|
static const char *const words[20] = {
|
|
"this", "is", "a", "test",
|
|
"broadcast", "the", "emergency", "of",
|
|
"system", "quick", "brown", "fox",
|
|
"jumped", "over", "lazy", "dogs",
|
|
"back", "January", "Febuary", "March"
|
|
};
|
|
static const char *const lines[20] = {
|
|
"You should have received a copy of the GNU General Public License\n",
|
|
"char c, cm, *cmd, *cmd1;\n",
|
|
"generate a command by percentages\n",
|
|
"Numbers may be typed as a prefix to some commands.\n",
|
|
"Quit, discarding changes!\n",
|
|
"Forced write, if permission originally not valid.\n",
|
|
"In general, any ex or ed command (such as substitute or delete).\n",
|
|
"I have tickets available for the Blazers vs LA Clippers for Monday, Janurary 1 at 1:00pm.\n",
|
|
"Please get w/ me and I will go over it with you.\n",
|
|
"The following is a list of scheduled, committed changes.\n",
|
|
"1. Launch Norton Antivirus (Start, Programs, Norton Antivirus)\n",
|
|
"Reminder....Town Meeting in Central Perk cafe today at 3:00pm.\n",
|
|
"Any question about transactions please contact Sterling Huxley.\n",
|
|
"I will try to get back to you by Friday, December 31.\n",
|
|
"This Change will be implemented on Friday.\n",
|
|
"Let me know if you have problems accessing this;\n",
|
|
"Sterling Huxley recently added you to the access list.\n",
|
|
"Would you like to go to lunch?\n",
|
|
"The last command will be automatically run.\n",
|
|
"This is too much english for a computer geek.\n",
|
|
};
|
|
static char *multilines[20] = {
|
|
"You should have received a copy of the GNU General Public License\n",
|
|
"char c, cm, *cmd, *cmd1;\n",
|
|
"generate a command by percentages\n",
|
|
"Numbers may be typed as a prefix to some commands.\n",
|
|
"Quit, discarding changes!\n",
|
|
"Forced write, if permission originally not valid.\n",
|
|
"In general, any ex or ed command (such as substitute or delete).\n",
|
|
"I have tickets available for the Blazers vs LA Clippers for Monday, Janurary 1 at 1:00pm.\n",
|
|
"Please get w/ me and I will go over it with you.\n",
|
|
"The following is a list of scheduled, committed changes.\n",
|
|
"1. Launch Norton Antivirus (Start, Programs, Norton Antivirus)\n",
|
|
"Reminder....Town Meeting in Central Perk cafe today at 3:00pm.\n",
|
|
"Any question about transactions please contact Sterling Huxley.\n",
|
|
"I will try to get back to you by Friday, December 31.\n",
|
|
"This Change will be implemented on Friday.\n",
|
|
"Let me know if you have problems accessing this;\n",
|
|
"Sterling Huxley recently added you to the access list.\n",
|
|
"Would you like to go to lunch?\n",
|
|
"The last command will be automatically run.\n",
|
|
"This is too much english for a computer geek.\n",
|
|
};
|
|
|
|
// create a random command to execute
|
|
static void crash_dummy()
|
|
{
|
|
static int sleeptime; // how long to pause between commands
|
|
char c, cm, *cmd, *cmd1;
|
|
int i, cnt, thing, rbi, startrbi, percent;
|
|
|
|
// "dot" movement commands
|
|
cmd1 = " \n\r\002\004\005\006\025\0310^$-+wWeEbBhjklHL";
|
|
|
|
// is there already a command running?
|
|
if (readbuffer[0] > 0)
|
|
goto cd1;
|
|
cd0:
|
|
readbuffer[0] = 'X';
|
|
startrbi = rbi = 1;
|
|
sleeptime = 0; // how long to pause between commands
|
|
memset(readbuffer, '\0', sizeof(readbuffer));
|
|
// generate a command by percentages
|
|
percent = (int) lrand48() % 100; // get a number from 0-99
|
|
if (percent < Mp) { // Movement commands
|
|
// available commands
|
|
cmd = cmd1;
|
|
M++;
|
|
} else if (percent < Np) { // non-movement commands
|
|
cmd = "mz<>\'\""; // available commands
|
|
N++;
|
|
} else if (percent < Dp) { // Delete commands
|
|
cmd = "dx"; // available commands
|
|
D++;
|
|
} else if (percent < Ip) { // Inset commands
|
|
cmd = "iIaAsrJ"; // available commands
|
|
I++;
|
|
} else if (percent < Yp) { // Yank commands
|
|
cmd = "yY"; // available commands
|
|
Y++;
|
|
} else if (percent < Pp) { // Put commands
|
|
cmd = "pP"; // available commands
|
|
P++;
|
|
} else {
|
|
// We do not know how to handle this command, try again
|
|
U++;
|
|
goto cd0;
|
|
}
|
|
// randomly pick one of the available cmds from "cmd[]"
|
|
i = (int) lrand48() % strlen(cmd);
|
|
cm = cmd[i];
|
|
if (strchr(":\024", cm))
|
|
goto cd0; // dont allow colon or ctrl-T commands
|
|
readbuffer[rbi++] = cm; // put cmd into input buffer
|
|
|
|
// now we have the command-
|
|
// there are 1, 2, and multi char commands
|
|
// find out which and generate the rest of command as necessary
|
|
if (strchr("dmryz<>\'\"", cm)) { // 2-char commands
|
|
cmd1 = " \n\r0$^-+wWeEbBhjklHL";
|
|
if (cm == 'm' || cm == '\'' || cm == '\"') { // pick a reg[]
|
|
cmd1 = "abcdefghijklmnopqrstuvwxyz";
|
|
}
|
|
thing = (int) lrand48() % strlen(cmd1); // pick a movement command
|
|
c = cmd1[thing];
|
|
readbuffer[rbi++] = c; // add movement to input buffer
|
|
}
|
|
if (strchr("iIaAsc", cm)) { // multi-char commands
|
|
if (cm == 'c') {
|
|
// change some thing
|
|
thing = (int) lrand48() % strlen(cmd1); // pick a movement command
|
|
c = cmd1[thing];
|
|
readbuffer[rbi++] = c; // add movement to input buffer
|
|
}
|
|
thing = (int) lrand48() % 4; // what thing to insert
|
|
cnt = (int) lrand48() % 10; // how many to insert
|
|
for (i = 0; i < cnt; i++) {
|
|
if (thing == 0) { // insert chars
|
|
readbuffer[rbi++] = chars[((int) lrand48() % strlen(chars))];
|
|
} else if (thing == 1) { // insert words
|
|
strcat(readbuffer, words[(int) lrand48() % 20]);
|
|
strcat(readbuffer, " ");
|
|
sleeptime = 0; // how fast to type
|
|
} else if (thing == 2) { // insert lines
|
|
strcat(readbuffer, lines[(int) lrand48() % 20]);
|
|
sleeptime = 0; // how fast to type
|
|
} else { // insert multi-lines
|
|
strcat(readbuffer, multilines[(int) lrand48() % 20]);
|
|
sleeptime = 0; // how fast to type
|
|
}
|
|
}
|
|
strcat(readbuffer, ESC);
|
|
}
|
|
readbuffer[0] = strlen(readbuffer + 1);
|
|
cd1:
|
|
totalcmds++;
|
|
if (sleeptime > 0)
|
|
mysleep(sleeptime); // sleep 1/100 sec
|
|
}
|
|
|
|
// test to see if there are any errors
|
|
static void crash_test()
|
|
{
|
|
static time_t oldtim;
|
|
|
|
time_t tim;
|
|
char d[2], msg[80];
|
|
|
|
msg[0] = '\0';
|
|
if (end < text) {
|
|
strcat(msg, "end<text ");
|
|
}
|
|
if (end > textend) {
|
|
strcat(msg, "end>textend ");
|
|
}
|
|
if (dot < text) {
|
|
strcat(msg, "dot<text ");
|
|
}
|
|
if (dot > end) {
|
|
strcat(msg, "dot>end ");
|
|
}
|
|
if (screenbegin < text) {
|
|
strcat(msg, "screenbegin<text ");
|
|
}
|
|
if (screenbegin > end - 1) {
|
|
strcat(msg, "screenbegin>end-1 ");
|
|
}
|
|
|
|
if (msg[0]) {
|
|
printf("\n\n%d: \'%c\' %s\n\n\n%s[Hit return to continue]%s",
|
|
totalcmds, last_input_char, msg, ESC_BOLD_TEXT, ESC_NORM_TEXT);
|
|
fflush_all();
|
|
while (safe_read(STDIN_FILENO, d, 1) > 0) {
|
|
if (d[0] == '\n' || d[0] == '\r')
|
|
break;
|
|
}
|
|
}
|
|
tim = time(NULL);
|
|
if (tim >= (oldtim + 3)) {
|
|
sprintf(status_buffer,
|
|
"Tot=%d: M=%d N=%d I=%d D=%d Y=%d P=%d U=%d size=%d",
|
|
totalcmds, M, N, I, D, Y, P, U, end - text + 1);
|
|
oldtim = tim;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#if ENABLE_FEATURE_VI_COLON
|
|
static void run_cmds(char *p)
|
|
{
|
|
while (p) {
|
|
char *q = p;
|
|
p = strchr(q, '\n');
|
|
if (p)
|
|
while (*p == '\n')
|
|
*p++ = '\0';
|
|
if (strlen(q) < MAX_INPUT_LEN)
|
|
colon(q);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void edit_file(char *fn)
|
|
{
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
#define cur_line edit_file__cur_line
|
|
#endif
|
|
int c;
|
|
#if ENABLE_FEATURE_VI_USE_SIGNALS
|
|
int sig;
|
|
#endif
|
|
|
|
editing = 1; // 0 = exit, 1 = one file, 2 = multiple files
|
|
rawmode();
|
|
rows = 24;
|
|
columns = 80;
|
|
IF_FEATURE_VI_ASK_TERMINAL(G.get_rowcol_error =) query_screen_dimensions();
|
|
#if ENABLE_FEATURE_VI_ASK_TERMINAL
|
|
if (G.get_rowcol_error /* TODO? && no input on stdin */) {
|
|
uint64_t k;
|
|
write1(ESC"[999;999H" ESC"[6n");
|
|
fflush_all();
|
|
k = safe_read_key(STDIN_FILENO, readbuffer, /*timeout_ms:*/ 100);
|
|
if ((int32_t)k == KEYCODE_CURSOR_POS) {
|
|
uint32_t rc = (k >> 32);
|
|
columns = (rc & 0x7fff);
|
|
if (columns > MAX_SCR_COLS)
|
|
columns = MAX_SCR_COLS;
|
|
rows = ((rc >> 16) & 0x7fff);
|
|
if (rows > MAX_SCR_ROWS)
|
|
rows = MAX_SCR_ROWS;
|
|
}
|
|
}
|
|
#endif
|
|
new_screen(rows, columns); // get memory for virtual screen
|
|
init_text_buffer(fn);
|
|
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
YDreg = 26; // default Yank/Delete reg
|
|
// Ureg = 27; - const // hold orig line for "U" cmd
|
|
mark[26] = mark[27] = text; // init "previous context"
|
|
#endif
|
|
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
last_input_char = '\0';
|
|
#endif
|
|
crow = 0;
|
|
ccol = 0;
|
|
|
|
#if ENABLE_FEATURE_VI_USE_SIGNALS
|
|
signal(SIGWINCH, winch_handler);
|
|
signal(SIGTSTP, tstp_handler);
|
|
sig = sigsetjmp(restart, 1);
|
|
if (sig != 0) {
|
|
screenbegin = dot = text;
|
|
}
|
|
// int_handler() can jump to "restart",
|
|
// must install handler *after* initializing "restart"
|
|
signal(SIGINT, int_handler);
|
|
#endif
|
|
|
|
cmd_mode = 0; // 0=command 1=insert 2='R'eplace
|
|
cmdcnt = 0;
|
|
offset = 0; // no horizontal offset
|
|
c = '\0';
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
free(ioq_start);
|
|
ioq_start = NULL;
|
|
adding2q = 0;
|
|
#endif
|
|
|
|
#if ENABLE_FEATURE_VI_COLON
|
|
while (initial_cmds)
|
|
run_cmds((char *)llist_pop(&initial_cmds));
|
|
#endif
|
|
redraw(FALSE); // dont force every col re-draw
|
|
//------This is the main Vi cmd handling loop -----------------------
|
|
while (editing > 0) {
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
if (crashme > 0) {
|
|
if ((end - text) > 1) {
|
|
crash_dummy(); // generate a random command
|
|
} else {
|
|
crashme = 0;
|
|
string_insert(text, "\n\n##### Ran out of text to work on. #####\n\n", NO_UNDO);
|
|
dot = text;
|
|
refresh(FALSE);
|
|
}
|
|
}
|
|
#endif
|
|
c = get_one_char(); // get a cmd from user
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
last_input_char = c;
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_YANKMARK
|
|
// save a copy of the current line- for the 'U" command
|
|
if (begin_line(dot) != cur_line) {
|
|
cur_line = begin_line(dot);
|
|
text_yank(begin_line(dot), end_line(dot), Ureg, PARTIAL);
|
|
}
|
|
#endif
|
|
#if ENABLE_FEATURE_VI_DOT_CMD
|
|
// If c is a command that changes text[],
|
|
// (re)start remembering the input for the "." command.
|
|
if (!adding2q
|
|
&& ioq_start == NULL
|
|
&& cmd_mode == 0 // command mode
|
|
&& c > '\0' // exclude NUL and non-ASCII chars
|
|
&& c < 0x7f // (Unicode and such)
|
|
&& strchr(modifying_cmds, c)
|
|
) {
|
|
start_new_cmd_q(c);
|
|
}
|
|
#endif
|
|
do_cmd(c); // execute the user command
|
|
|
|
// poll to see if there is input already waiting. if we are
|
|
// not able to display output fast enough to keep up, skip
|
|
// the display update until we catch up with input.
|
|
if (!readbuffer[0] && mysleep(0) == 0) {
|
|
// no input pending - so update output
|
|
refresh(FALSE);
|
|
show_status_line();
|
|
}
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
if (crashme > 0)
|
|
crash_test(); // test editor variables
|
|
#endif
|
|
}
|
|
//-------------------------------------------------------------------
|
|
|
|
go_bottom_and_clear_to_eol();
|
|
cookmode();
|
|
#undef cur_line
|
|
}
|
|
|
|
#define VI_OPTSTR \
|
|
IF_FEATURE_VI_CRASHME("C") \
|
|
IF_FEATURE_VI_COLON("c:*") \
|
|
"Hh" \
|
|
IF_FEATURE_VI_READONLY("R")
|
|
|
|
enum {
|
|
IF_FEATURE_VI_CRASHME(OPTBIT_C,)
|
|
IF_FEATURE_VI_COLON(OPTBIT_c,)
|
|
OPTBIT_H,
|
|
OPTBIT_h,
|
|
IF_FEATURE_VI_READONLY(OPTBIT_R,)
|
|
OPT_C = IF_FEATURE_VI_CRASHME( (1 << OPTBIT_C)) + 0,
|
|
OPT_c = IF_FEATURE_VI_COLON( (1 << OPTBIT_c)) + 0,
|
|
OPT_H = 1 << OPTBIT_H,
|
|
OPT_h = 1 << OPTBIT_h,
|
|
OPT_R = IF_FEATURE_VI_READONLY( (1 << OPTBIT_R)) + 0,
|
|
};
|
|
|
|
int vi_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
|
|
int vi_main(int argc, char **argv)
|
|
{
|
|
int opts;
|
|
|
|
INIT_G();
|
|
|
|
#if ENABLE_FEATURE_VI_UNDO
|
|
//undo_stack_tail = NULL; - already is
|
|
# if ENABLE_FEATURE_VI_UNDO_QUEUE
|
|
undo_queue_state = UNDO_EMPTY;
|
|
//undo_q = 0; - already is
|
|
# endif
|
|
#endif
|
|
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
srand((long) getpid());
|
|
#endif
|
|
#ifdef NO_SUCH_APPLET_YET
|
|
// if we aren't "vi", we are "view"
|
|
if (ENABLE_FEATURE_VI_READONLY && applet_name[2]) {
|
|
SET_READONLY_MODE(readonly_mode);
|
|
}
|
|
#endif
|
|
|
|
// 0: all of our options are disabled by default in vim
|
|
//vi_setops = 0;
|
|
opts = getopt32(argv, VI_OPTSTR IF_FEATURE_VI_COLON(, &initial_cmds));
|
|
|
|
#if ENABLE_FEATURE_VI_CRASHME
|
|
if (opts & OPT_C)
|
|
crashme = 1;
|
|
#endif
|
|
if (opts & OPT_R)
|
|
SET_READONLY_MODE(readonly_mode);
|
|
if (opts & OPT_H)
|
|
show_help();
|
|
if (opts & (OPT_H | OPT_h)) {
|
|
bb_show_usage();
|
|
return 1;
|
|
}
|
|
|
|
argv += optind;
|
|
cmdline_filecnt = argc - optind;
|
|
|
|
// 1- process EXINIT variable from environment
|
|
// 2- if EXINIT is unset process $HOME/.exrc file
|
|
// 3- process command line args
|
|
#if ENABLE_FEATURE_VI_COLON
|
|
{
|
|
const char *exinit = getenv("EXINIT");
|
|
char *cmds = NULL;
|
|
|
|
if (exinit) {
|
|
cmds = xstrdup(exinit);
|
|
} else {
|
|
const char *home = getenv("HOME");
|
|
|
|
if (home && *home) {
|
|
char *exrc = concat_path_file(home, ".exrc");
|
|
struct stat st;
|
|
|
|
// .exrc must belong to and only be writable by user
|
|
if (stat(exrc, &st) == 0) {
|
|
if ((st.st_mode & (S_IWGRP|S_IWOTH)) == 0
|
|
&& st.st_uid == getuid()
|
|
) {
|
|
cmds = xmalloc_open_read_close(exrc, NULL);
|
|
} else {
|
|
status_line_bold(".exrc: permission denied");
|
|
}
|
|
}
|
|
free(exrc);
|
|
}
|
|
}
|
|
|
|
if (cmds) {
|
|
init_text_buffer(NULL);
|
|
run_cmds(cmds);
|
|
free(cmds);
|
|
}
|
|
}
|
|
#endif
|
|
// "Save cursor, use alternate screen buffer, clear screen"
|
|
write1(ESC"[?1049h");
|
|
// This is the main file handling loop
|
|
optind = 0;
|
|
while (1) {
|
|
edit_file(argv[optind]); // might be NULL on 1st iteration
|
|
// NB: optind can be changed by ":next" and ":rewind" commands
|
|
optind++;
|
|
if (optind >= cmdline_filecnt)
|
|
break;
|
|
}
|
|
// "Use normal screen buffer, restore cursor"
|
|
write1(ESC"[?1049l");
|
|
|
|
return 0;
|
|
}
|