;**
;**	Composite VI emulator.  Based on the work of Richard M. Goodin,
;**	whom I tried to reach for permission many times (but could not
;**	contact!) and Douglas MacLean.  Rewritten and converted to BRIEF
;**	v2.01 (and above) by Dan Teven, UnderWare, Inc.
;**
;**	Rather than try to duplicate everything about VI, I've attempted
;**	to compromise by using built-in features whenever they approximate
;**	VI's.  (Having never used VI, I found compromising easy.)  These
;**	new macros take far less memory (only two global strings!) and run
;**	faster than their predecessors.  I have fixed some bugs, but
;**	probably added just as many--please address all bug reports to me
;**	care of the Solution Systems BBS.
;**
;**	New features include help that is automatically integrated with
;**	BRIEF's built-in help and the ability to have both BRIEF and VI
;**	environments around simultaneously.
;**
;**	To use this package, follow these steps:
;**
;**	1.  Make sure you have version 2.01 or above of BRIEF.
;**
;**	2.  Copy vi.cm and vi2.cm into a directory on your BPATH.
;**
;**	3.  Copy vi.txt and vi.mnu into your BHELP directory.
;**
;**	4.  To start VI emulation from within BRIEF, just run the "vi"
;**	command.  To return to the normal BRIEF environment, you can
;**	press <Alt-X>.  (To make the VI environment your default, just
;**	add -mvi to your BFLAGS.)
;**

#include "vi.h"

(macro _init
	(
		(int		vi_prefix						;** Saved prefixed command.
					vi_ind_mode						;** Is auto-indenting on?
					vi_rep_cnt						;** # of times to repeat next cmd.
					vi_srch_chr						;** Last character searched for.
					vi_chr_dir						;** Character search direction.
					vi_mode							;** Current keyboard mode.
					vi_cmd_kbd						;** ID of command mode keymap.
					vi_ins_kbd						;** ID of insert mode keymap.
					vi_rep_kbd						;** ID of replace mode keymap.
					vi_brief_kbd					;** Regular BRIEF keymap.
		)
		(string	vi_buf_prefix					;** Current scrap buffer name.
					vi_dos_cmd 						;** Last DOS command run.
		)
		(global	vi_prefix
					vi_ind_mode
					vi_rep_cnt
					vi_srch_chr
					vi_chr_dir
					vi_mode
					vi_cmd_kbd
					vi_ins_kbd
					vi_rep_kbd
					vi_brief_kbd
					vi_buf_prefix
					vi_dos_cmd
		)
		;**	Load necessary additional macros.  Make sure that vi2,
		;**	which contains half the package, is found.

		(message "Loading VI emulation package...")

		(if (&& (<= (load_macro "vi2") 0)
			(&& (get_parm 0 vi_dos_cmd "Enter full path name for vi2.cm: ")
			(<= (load_macro vi_dos_cmd) 0)))
			(
				(error "Unable to load VI emulator.")
				(return)
			)
		)
		(autoload "indent" "r_indent")
		(pause_on_error TRUE)
		(= vi_brief_kbd (inq_keyboard))

		;**	Create 3 different keymaps, one for VI command mode, one
		;**	for insert mode, and one for replace mode.  These will
		;**	be allowed to coexist with the normal BRIEF keyboard.

		(keyboard_push)
		(assign_to_key "<Ctrl-B>" "vi_repeat page_up")
		(assign_to_key "<Ctrl-C>"  "search_case")
		(assign_to_key "<Ctrl-D>" "vi_repeat \"scroll_down 0\"")
		(assign_to_key "<Ctrl-E>" "ooze_down")
		(assign_to_key "<Ctrl-F>" "vi_repeat page_down")
		(assign_to_key "<Ctrl-H>" "vi_repeat left")
		(assign_to_key "<Backspace>" "vi_repeat left")
		(assign_to_key "<Ctrl-I>" "vi_repeat vi_tab")
		(assign_to_key "<Tab>" "vi_repeat vi_tab")
		(assign_to_key "<Ctrl-J>" "vi_repeat down")
		(assign_to_key "<Ctrl-Enter>" "prev_white")
		(assign_to_key "<Ctrl-K>" "vi_repeat up")
		(assign_to_key "<Ctrl-L>" "vi_repeat right")
		(assign_to_key "<Enter>" "next_white")
		(assign_to_key "<Ctrl-P>" "playback")
		(assign_to_key "<Ctrl-R>" "remember")
		(assign_to_key "<Ctrl-U>" "vi_repeat \"scroll_up 0\"")
		(assign_to_key "<Ctrl-Y>" "ooze_up")
		(assign_to_key "<Ctrl-Z>" "dos")
		(assign_to_key "<Esc>" "vi_esc")
		(assign_to_key " " "vi_repeat right")
		(assign_to_key "\"" "vi_buffer")
		(assign_to_key "$" "vi_eol")
		(assign_to_key "\\%" "vi_match")
		(assign_to_key "'" "vi_do_mark 1")
		(assign_to_key "+" "next_white")
		(assign_to_key "," "vi_chr_back")
		(assign_to_key "-" "prev_white")
		(assign_to_key "/" "vi_srch_fwd")
		(assign_to_key "0" "vi_0_key")
		(assign_to_key "1" "vi_digit 1")
		(assign_to_key "2" "vi_digit 2")
		(assign_to_key "3" "vi_digit 3")
		(assign_to_key "4" "vi_digit 4")
		(assign_to_key "5" "vi_digit 5")
		(assign_to_key "6" "vi_digit 6")
		(assign_to_key "7" "vi_digit 7")
		(assign_to_key "8" "vi_digit 8")
		(assign_to_key "9" "vi_digit 9")
		(assign_to_key ":" "vi_command")
		(assign_to_key ";" "vi_chr_srch 0")
		(assign_to_key "\\<" "vi_indent 1")
		(assign_to_key "\\>" "vi_indent 0")
		(assign_to_key "?" "vi_srch_back")
		(assign_to_key "@" "execute_macro")
		(assign_to_key "A" "vi_append_eol")
		(assign_to_key "B" "vi_word 0 1 0")
		(assign_to_key "C" "vi_change_to_eol")
		(assign_to_key "D" "vi_delete_to_eol")
		(assign_to_key "E" "vi_word 0 0 1")
		(assign_to_key "F" "vi_search 0 0")
		(assign_to_key "G" "vi_go")
		(assign_to_key "H" "vi_top_of_window")
		(assign_to_key "I" "vi_ins_after")
		(assign_to_key "J" "vi_join")
		(assign_to_key "L" "vi_end_of_window")
		(assign_to_key "M" "vi_middle_of_window")
		(assign_to_key "N" "vi_str_back")
		(assign_to_key "O" "vi_open_line 1")
		(assign_to_key "P" "vi_paste 0")
		(assign_to_key "R" "vi_rep")
		(assign_to_key "S" "vi_subst_lines")
		(assign_to_key "T" "vi_search 0 1")
		(assign_to_key "U" "undo")
		(assign_to_key "W" "vi_word 0 0 0")
		(assign_to_key "X" "vi_backchr")
		(assign_to_key "Y" "vi_yank_lines")
		(assign_to_key "ZZ" "vi_write_quit")
		(assign_to_key "\\\\" "vi_set_tab")
		(assign_to_key "\\^" "vi_beg 1")
		(assign_to_key "`" "vi_do_mark 0")
		(assign_to_key "a" "vi_append")
		(assign_to_key "b" "vi_word 1 1 0")
		(assign_to_key "c" "vi_change")
		(assign_to_key "d" "vi_delete")
		(assign_to_key "e" "vi_word 1 0 1")
		(assign_to_key "f" "vi_search 1 0")
		(assign_to_key "g" "vi_go")
		(assign_to_key "h" "vi_repeat left")
		(assign_to_key "i" "vi_ins")
		(assign_to_key "j" "vi_repeat down")
		(assign_to_key "k" "vi_repeat up")
		(assign_to_key "l" "vi_repeat right")
		(assign_to_key "m" "vi_mark")
		(assign_to_key "n" "vi_str_srch")
		(assign_to_key "o" "vi_open_line 0")
		(assign_to_key "p" "vi_paste 1")
		(assign_to_key "r" "vi_repl_char")
		(assign_to_key "s" "vi_subst_chars")
		(assign_to_key "t" "vi_search 1 1")
		(assign_to_key "u" "undo")
		(assign_to_key "w" "vi_word 1 0 0")
		(assign_to_key "x" "vi_delchr")
		(assign_to_key "y" "vi_yank")
		(assign_to_key "z<Enter>" "to_top")
		(assign_to_key "z." "center_line")
		(assign_to_key "z-" "to_bottom")
		(assign_to_key "|" "vi_col")
		(assign_to_key "~" "vi_repeat vi_case")
		(assign_to_key "<Shift-Tab>" "vi_repeat back_tab")
		(assign_to_key "<Home>" "vi_top_of_window")
		(assign_to_key "<End>" "vi_eol")
		(assign_to_key "<PgUp>" "vi_repeat page_up")
		(assign_to_key "<PgDn>" "vi_repeat page_down")
		(assign_to_key "<Left>" "vi_repeat left")
		(assign_to_key "<Right>" "vi_repeat right")
		(assign_to_key "<Up>" "vi_repeat up")
		(assign_to_key "<Down>" "vi_repeat down")
		(assign_to_key "<Ins>" "vi_ins")
		(assign_to_key "<Del>" "delete_char")
		(assign_to_key "<Shift-Up>" "change_window 0")
		(assign_to_key "<Shift-Left>" "change_window 3")
		(assign_to_key "<Shift-Right>" "change_window 1")
		(assign_to_key "<Shift-Down>" "change_window 2")
		(assign_to_key "<Alt-C>" "copy")
		(assign_to_key "<Alt-D>" "vi_delete_block")
		(assign_to_key "<Alt-F>" "display_file_name")
		(assign_to_key "<Alt-H>" "vi_help")
		(assign_to_key "<Alt-K>" "cut")
		(assign_to_key "<Alt-M>" "mark")
		(assign_to_key "<Alt-O>" "print")
		(assign_to_key "<Alt-P>" "paste")
		(assign_to_key "<Alt-X>" "vi_return")
		(= vi_cmd_kbd (inq_keyboard))
		(keyboard_pop TRUE)

		(keyboard_push)
		(keyboard_typeables)
		(assign_to_key "<Tab>" "vi_ins_tab")
		(assign_to_key "<Enter>" "vi_ins_cr")
		(assign_to_key "<Esc>" "vi_ins_esc")
		(assign_to_key "<Shift-Tab>" "vi_ins_btab")
		(assign_to_key "<Left>" "left")
		(assign_to_key "<Right>" "right")
		(= vi_ins_kbd (inq_keyboard))
		(keyboard_pop TRUE)

		(keyboard_push)
		(keyboard_typeables)
		(assign_to_key "<Backspace>" "undo")
		(assign_to_key "<Enter>" "vi_ins_cr")
		(assign_to_key "<Esc>" "vi_rep_esc")
		(assign_to_key "<Left>" "left")
		(assign_to_key "<Right>" "right")
		(= vi_rep_kbd (inq_keyboard))
		(keyboard_pop TRUE)
	)
)

(macro vi
	(
		(insert_mode TRUE)
		(= vi_ind_mode TRUE)
		(register_macro IDLE_TIME_MACRO "vi_idle")
		(vi_ins_esc)
	)
)

;**	Return to normal BRIEF editing.

(macro vi_return
	(
		(keyboard_pop TRUE)
		(keyboard_push vi_brief_kbd)
		(unregister_macro IDLE_TIME_MACRO "vi_idle")
		(call_registered_macro NEW_FILE_MACRO)
		(message "VI emulation cancelled.")
	)
)

;**	Enter insert mode.  Assigned to i and Ins, and called from many
;**	other macros.

(macro vi_ins
	(
		(keyboard_pop TRUE)
		(keyboard_push vi_ins_kbd)
		(= vi_mode INSERT_MODE)
		(message "<INSERT>")
	)
)

;**	Enter replace mode.  Assigned to R.

(macro vi_rep
	(
		(if (== (read 1) "\n")
			(beep)
		;else
			(
				(keyboard_pop TRUE)
				(keyboard_push vi_rep_kbd)
				(insert_mode FALSE)
				(= vi_mode REPLACE_MODE)
				(message "<REPLACE>")
			)
		)
	)
)

;**	Called by Esc in command mode.  Just resets everything.

(macro vi_esc
	(
		(message "<COMMAND>")
		(vi_reset_cmd)
	)
)

;**	Called by Esc in insert mode.  Switches to command mode.

(macro vi_ins_esc
	(
		(keyboard_pop TRUE)
		(keyboard_push vi_cmd_kbd)
		(= vi_mode COMMAND_MODE)
		(vi_esc)
	)
)

;**	Called by Esc in replace mode.  Switches to command mode.

(macro vi_rep_esc
	(
		(insert_mode TRUE)
		(vi_ins_esc)
	)
)

;**	Called by Shift-Tab in insert mode.  Will delete back to either
;**	the last tab stop or the last nonwhite, whichever comes first.

(macro vi_ins_btab
	(
		(int		size)

		(extern	back_tab)

		(drop_anchor 4)
		(back_tab)

     (if (= size (inq_mark_size))
			(
				(next_char size)

				(while (>= (-- size) 0)
					(
						(left)

						(if (index " \t" (read 1))
							(delete_char)
						;else
							(break)
						)
					)
				)
			)
		)
		(raise_anchor)
	)
)

;**	Called by Tab, in insert mode.  Will insert spaces to the next
;**	tab stop.

(macro vi_ins_tab
	(
		(int		dist)

		(= dist (distance_to_tab))

		(while (>= (-- dist) 0)
			(insert " ")
		)
	)
)

;**	Called by Enter in insert mode or replace mode.  Uses BRIEF's
;**	built-in regular indenting.

(macro vi_ins_cr
	(
		(extern	r_indent)

		(if vi_ind_mode
			(r_indent)
		;else
			(insert "\n")
		)
	)
)

;**	After 5 seconds, show the current mode again.

(macro vi_idle
	(
		(if (== (inq_idle_time) 5)
			(switch vi_mode
				COMMAND_MODE
					(message "<COMMAND>")
				INSERT_MODE
					(message "<INSERT>")
				REPLACE_MODE
					(message "<REPLACE>")
			)
		)
	)
)


(macro vi_repeat
	(
		(string	command)

		(get_parm 0 command)
		(rep_zero)

		(while (>= (-- vi_rep_cnt) 0)
			(execute_macro command)
		)
		(vi_reset_cmd)
	)
)

;**	Moves the cursor to the first nonwhite character after the
;**	beginning of the current line; returns the string of white
;**	space before that character.

(macro vi_digit
	(
		(int		inchr)

		(get_parm 0 inchr)
		(= vi_rep_cnt (+ (* vi_rep_cnt 10) inchr))
	)
)

;**	Moves the cursor to the next tab stop.

(macro vi_tab
	(move_rel 0 (distance_to_tab))
)

(macro vi_col
	(
		(rep_zero)
		(move_abs 0 vi_rep_cnt)
		(vi_reset_cmd)
	)
)

;**	Join lines.  If automatic indenting is on, remove all white space
;**	from the end of the first and beginning of the second line, and
;**	replace it with a single space.

(macro vi_join
	(
		(rep_zero)
		(-- vi_rep_cnt)
		(rep_zero)
		(save_position)

		(while (>= (-- vi_rep_cnt) 0)
			(
				(end_of_line)

				(if vi_ind_mode
					(
						(search_back "<|{[~ \\t\\n]\\c}" TRUE TRUE FALSE)
						(delete_to_eol)
					)
				)
				(delete_char)

				(if vi_ind_mode
					(
						(insert " ")
						(while (== (read 1) " ")
							(delete_char)
						)
					)
				)
			)
		)
		(restore_position)
		(vi_reset_cmd)
	)
)

;**	Enter insert mode after the cursor position.

(macro vi_append
	(
		(next_char)
		(vi_ins)
	)
)

;**	Enter insert mode at the end of the line.

(macro vi_append_eol
	(
		(end_of_line)
		(vi_ins)
	)
)

;**	First non-white character on the line (or end of line).

(macro vi_white
	(
		(beginning_of_line)
		(search_fwd "[~ \t]")
	)
)

;**	Insert a line and go into insert mode.

(macro vi_open_line
	(
		(int		open_above)

		(get_parm 0 open_above)

		(if open_above
			(up)
		)
		(end_of_line)
		(vi_ins_cr)
		(vi_ins)
	)
)

;**	Replace a single character.

(macro vi_repl_char
	(
		(string	cmd)

		(int		inchr)

		(if (== (read 1) "\n")
			(beep)
		;else
			(if (== (& (= inchr (pause "vi_repl_char")) 0xff) 0)
				(call_registered_macro BAD_KEY_MACRO)
			;else
				(
					(save_position)
					(sprintf cmd "_vi_repl_char %c" (& inchr 0xff))
					(vi_repeat cmd)
					(restore_position)
				)
			)
		)
	)
)

(macro _vi_repl_char
	(
		(string	chr)

		(get_parm 0 chr)
		(delete_char)
		(insert chr)
	)
)

;**	Convert a single character to the other case.

(macro vi_case
	(
		(string	character)

		(= character (read 1))

		(if (&& (>= character "A") (<= character "z"))
			(if (<= character "Z")
				(_vi_repl_char (lower character))
			;else
				(if (>= character "a")
					(_vi_repl_char (upper character))
				;else
					(next_char)
				)
			)
		;else
			(next_char)
		)
	)
)

(macro vi_go
	(
		(if vi_rep_cnt
			(move_abs vi_rep_cnt 0)
		;else
			(end_of_buffer)
		)
		(vi_beg 1)
	)
)

(macro ooze_down
	(
		(rep_zero)
		(scroll_down vi_rep_cnt)
		(vi_reset_cmd)
	)
)

(macro ooze_up
	(
		(rep_zero)
		(scroll_up vi_rep_cnt)
		(vi_reset_cmd)
	)
)

;**	Scroll the screen down.  The default is one line.  If a number is
;**	passed, scroll that many lines; if it's zero, scroll half a window.

(macro scroll_down
	(
		(int		num_lines
					curr_line
					top_line
		)
		(= num_lines 1)

		(if (&& (get_parm 0 num_lines) (! num_lines))
			(
				(inq_window_size num_lines)
				(/= num_lines 2)
			)
		)
		(inq_position curr_line)
		(top_of_window)
		(inq_position top_line)
		(set_top_left (- top_line num_lines))
		(move_abs (- curr_line num_lines))
	)
)

(macro scroll_up
	(
		(int		num_lines
					curr_line
					top_line
		)
		(= num_lines 1)

		(if (&& (get_parm 0 num_lines) (! num_lines))
			(
				(inq_window_size num_lines)
				(/= num_lines 2)
			)
		)
		(inq_position curr_line)
		(top_of_window)
		(inq_position top_line)
		(set_top_left (+ top_line num_lines))
		(move_abs (+ curr_line num_lines))
	)
)

;**	Top of window, or <repeatcount> lines below it, then to the next
;**	white space character on that line.

(macro vi_top_of_window
	(
		(top_of_window)
		(rep_zero)

		(if (> (-- vi_rep_cnt) 0)
			(next_white)
		;else
			(vi_white)
		)
		(vi_reset_cmd)
	)
)

;**	Top of window, or <repeatcount> lines above it, then to the next
;**	white space character on that line.

(macro vi_end_of_window
	(
		(end_of_window)
		(rep_zero)

		(if (> (-- vi_rep_cnt) 0)
			(prev_white)
		;else
			(vi_white)
		)
		(vi_reset_cmd)
	)
)

;**	Middle of window, or <repeatcount> lines below it, then to the next
;**	white space character on that line.

(macro vi_middle_of_window
	(
		(int		lines)

		(rep_zero)
		(top_of_window)
		(inq_window_size lines)

		(if (/= lines 2)
			(-- lines)
		)
		(move_rel (+ lines (- vi_rep_cnt 1)) 0)
		(vi_white)
		(vi_reset_cmd)
	)
)

;**	Delete a character to a scrap buffer, then enter insert mode.

(macro vi_subst_chars
	(
		(vi_delchr)
		(vi_ins)
	)
)

;**	Handle the 0 key: if we have a repeat count, it's a digit; if not,
;**	call beginning_of_line.

(macro vi_0_key
	(if vi_rep_cnt
		(vi_digit 0)
	;else
		(vi_beg)
	)
)

;**	Beginning of line.  If a TRUE parameter is passed, go to the first
;**	non-white character instead.

(macro vi_beg
	(
		(int		nonwhite)

		(get_parm 0 nonwhite)

		(if vi_prefix
			(
				(save_position)
				(prev_char)
				(drop_anchor NORMAL)

				(if nonwhite
					(vi_white)
				;else
					(beginning_of_line)
				)
				(if (== vi_prefix YANK)
					(
						(vi_copy)
						(restore_position)
						(message "Yanked to beginning of line.")
					)
				;else
					(
						(vi_cut)
						(restore_position 0)
						(message "Deleted to beginning of line.")

						(if (== vi_prefix CHANGE)
							(vi_ins)
						)
					)
				)
			)
		;else
			(if nonwhite
				(vi_white)
			;else
				(beginning_of_line)
			)
		)
		(vi_reset_cmd)
	)
)

;**	Delete characters, but not past a newline.

(macro vi_delchr
	(
		(if (== (read 1) "\n")
			(beep)
		;else
			(
				(rep_zero)
				(drop_anchor NORMAL)

				(while (-- vi_rep_cnt)
					(
						(right)

						(if (== (read 1) "\n")
							(left)
						)
					)
				)
				(vi_cut)
				(vi_reset_cmd)
			)
		)
	)
)

(macro vi_backchr
	(
		(if (left)
			(
				(drop_anchor NORMAL)
				(rep_zero)

				(while (-- vi_rep_cnt)
					(left)
				)
				(vi_cut)
				(vi_reset_cmd)
			)
		;else
			(beep)
		)
	)
)

;**	Insert (non-white delimited) after cursor

(macro vi_ins_after
	(
		(next_char)
		(search_fwd "[~ \t]")
		(vi_ins)
	)
)

(macro next_white
	(
		(vi_repeat "down")
		(vi_white)
	)
)

(macro prev_white
	(
		(vi_repeat "up")
		(vi_white)
	)
)

(macro vi_set_tab
	(
		(if (<= vi_rep_cnt 0)
			(= vi_rep_cnt DEFSETAB)
		)
		(tabs (++ vi_rep_cnt))
		(vi_reset_cmd)
	)
)


;**
;**	Macros to integrate VI with BRIEF help system:
;**		vi_help checks to see if a repeat count or operator is pending
;**	and gives context-specific help on those subjects if so; if not,
;**	it brings up the help menu.
;**		_help_add_button is called when the dialog manager creates a
;**	menu; it waits for the main help menu, and adds the VI Emulation
;**	button to it.  This leads to another menu of VI commands, with
;**	help information in vi.txt.  (_help_add_button is a replacement
;**	macro.)
;**		pause is used for context-sensitive help, since VI bypasses
;**	get_parm.  (The normal way to integrate context-sensitive help
;**	would be to write a replacement for _context_help.)
;**

(extern	help
			_user_kbd
			display_help
)

(macro vi_help
	(if vi_prefix
		(
			(= _user_kbd (inq_keyboard))
			(display_help "operators" "vi.txt")
		)
	;else
		(help)
	)
)

(macro _help_add_button
	(
		(string	menu_name)

		(get_parm 0 menu_name)

		(if (== menu_name "Help Menu")
			(
				(search_fwd "<[ \t]@Windows" TRUE)
				(insert "   VI Emulation    ;process_help_menu \"VI\" \"vi.mnu\"\n")
			)
		)
		(returns (_help_add_button menu_name))
	)
)

;**	Wait for a key, then return it (unless it's Alt-H).

(macro pause
	(
		(int		key)

		(string	help_string
					old_msg
		)
		(while TRUE
			(
				(while (! (inq_kbd_char)))

				(if (== (= key (read_char)) (key_to_int "<Alt-H>"))
					(
						(= _user_kbd (inq_keyboard))
						(= old_msg (inq_message))
						(get_parm 0 help_string)
						(display_help help_string "vi.txt")
						(message old_msg)
					)
				;else
					(
						(returns key)
						(break)
					)
				)
			)
		)
	)
)
