	----------------------------------------------------------
	--       This Euphoria Editor was developed by          --
	--            Rapid Deployment Software.                --
	--                                                      --
	-- Permission is freely granted to anyone to modify     --
	-- and/or redistribute this editor (ed.ex, syncolor.e). --
	----------------------------------------------------------

-- Version: edu 3.20 for Euphoria 3.1.1, February/21/2022, Shian Lee.
--
-- Note:    If you update any include\*.doc file, then you should also run
--          bin\eduhelp.ex to update the index file edu.hlp (for control-K).
--
--          What's new in version 3.20:
--          * Euphoria keywords are indexed for faster help using control-K
--
-- Details: * edu.ex is a UTF-8 version of ed.ex, for UTF-8 encoding system
--            such as Linux Xterm terminal. edu supports also ASCII mode.
--            To run edu in UTF-8 mode use the "-u" command-line switch, e.g.:
--             exu edu -u filename.e
--
--          * edu.ex version 3.16 and later supports Long File Names on DOS32.
--
--          * If you run edu.ex on Windows-10, you should run it with eduw.bat
--            (not with edu.bat).
--
--          * Get context sensitive help by pressing control-K on current word.
--            If Keyword not found then a list of all doc files will be opened
--            in a cloned window. (you are limited to 10 windows...).
--          * Start/Stop recording a macro with control-W; and execute the
--            macro with control-O.
--          * Type in reverse direction with control-F (freeze cursor). It's
--            useful for right-to-left languages. Note: in UTF-8 mode the
--            string may appear reversed in other editors.
--
--          * file-name, file-lines and history strings are all stored as
--            unicode-strings in memory.
--            utf8-strings are converted to unicode-strings after input
--            with gets() and get_key(); and unicode-strings are converted
--            back to utf8-strings before output with puts() and also for
--            system() and open() commands.
--          * wrap() and scroll() are not compatible with UTF-8 multi-byte
--            character strings, so: wrap(0) is replaced with putsu(), and
--            scroll() is replaced with DisplayWindow() where needed.
--          * Most of the code in edu is not even "aware" that it's editing
--            unicode-strings.
--
-- Note:    * In Linux, if you pressed Ctrl-Z by mistake, you can return to
--            edu.ex by entering "fg" in the command line, and then press
--            Esc-Enter to return to the editor.
--          * In Linux Xterm terminal Chinese/Japanese/Korean are supported
--            using cjk_char().
--          * In general, edu.ex supports the very basic Unicode/UTF-8 features,
--            for more info see: http://www.cl.cam.ac.uk/~mgk25/unicode.html.
--          * In Xterm terminal you can also use the mouse to copy/paste UTF-8
--            strings from/into edu.
--
-- This program can be run with:
--     ex edu.ex (quick response on 95/98/ME. ex does not work on Windows-10)
-- or
--     exu edu.ex  (Linux / FreeBSD)
-- or
--     exwc edu.ex (quicker response on XP / Windows-10,
--                 but some control-key combinations aren't recognized)
--
-- To run in UTF-8 mode add the -u flag, for example:
--     exu edu.ex -u  (Linux / FreeBSD)
--
-- How it Works:
-- * Using gets(), ed reads and appends each line of text into a 2-d "buffer",
--   i.e. a sequence of sequences where each (sub)sequence contains one line.
-- * ed waits for you to press a key, and then fans out to one of many small
--   routines that each perform one editing operation.
-- * Each editing operation is responsible for updating the 2-d buffer variable
--   containing the lines of text, and for updating the screen to reflect any
--   changes. This code is typically fairly simple, but there can be a lot
--   of special cases to worry about.
-- * Finally, ed writes back each line, using puts()
-- * How multiple-windows works: When you switch to a new window, all the
--   variables associated with the current window are bundled together and
--   saved in a sequence, along with the 2-d text buffer for that window.
--   When you switch back, all of these state variables, plus the 2-d text
--   buffer variable for that window are restored. Most of the code in ed is
--   not even "aware" that there can be multiple-windows.

without type_check -- makes it a bit faster

include graphics.e
include get.e
include file.e
include wildcard.e
include dll.e
include machine.e   -- for tick_rate()
-- Lib2
include utf8.e      -- unicode_to_utf8(), utf8_to_unicode()
include machine2.e
include datetime.e
include file2.e  -- LFN support for DOS32, and more
include doserror.e -- critical-error handler for DOS32


constant MAX_UNICODE = #10FFFF -- last Unicode code point for UTF-8 encoding

constant EUDIR = getenv("EUDIR"),
	 EUDIR_NOT_SET = "EUDIR not set. See install.doc"

-- special input characters
constant CONTROL_B = 2,
	 CONTROL_C = 3,
	 CONTROL_D = 4,   -- alternate key for line-delete
	 CONTROL_F = 6,   -- freeze cursor while typing, for right-to-left language
	 CONTROL_K = 11,  -- context sensitive help (Keyword)
	 CONTROL_L = 12,
	 CONTROL_O = 15,  -- execute recorded macro (Shian)
	 CONTROL_P = 16,  -- alternate key for PAGE-DOWN in Linux.
			  -- DOS uses this key for printing or something.
	 CONTROL_R = 18,
	 CONTROL_T = 20,
	 CONTROL_U = 21,  -- alternate key for PAGE-UP in Linux
	 CONTROL_W = 23,  -- start/stop recording macro (Shian)
	 MACRO_REC_KEY = CONTROL_W,
	 MACRO_EXEC_KEY = CONTROL_O

constant CAPS_LOCK = 314  -- exwc only

constant SPACE = 32, TAB = '\t' -- constants for clarity (Shian)


integer ESCAPE, CR, BS,
	CONTROL_DELETE  -- key for line-delete
			-- (not available on some systems)

-- These are sequence for compatibility with Linux various terminals...(Shian)
sequence HOME, END, CONTROL_HOME, CONTROL_END,
	 PAGE_UP, PAGE_DOWN, INSERT,
	 DELETE, XDELETE, ARROW_LEFT, ARROW_RIGHT,
	 CONTROL_ARROW_LEFT, CONTROL_ARROW_RIGHT, ARROW_UP, ARROW_DOWN,
	 F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12

-- sequence delete_cmd -- using delete_file(), file2lfn.e
sequence compare_cmd
integer SAFE_CHAR -- minimum ASCII char that's safe to display
constant SLASH = OS_SLASH

if OS_LINUX then
--  SLASH = '/'
    SAFE_CHAR = 32
--  delete_cmd = "rm "
    compare_cmd = "diff "
    ESCAPE = 27
    CR = 10
    BS = 127 -- 263
    CONTROL_DELETE = 4  -- key for line-delete
			-- (not available on some systems)

--bookmark: Keys for Linux/FreeBSD terminals;
    -- These are sequence for compatibility with Linux various terminals...
    -- * Use 'euphoria/bin/xkey.ex' TO ADD MORE SEQUENCES FOR YOUR SYSTEM.
    -- * Supports Linux xterm/tty1-7 by default.
    -- * The *FIRST* atom in any sequences is the return value by next_key().
    HOME = {262, {27,91,72}, {27,91,49,126}}
    END = {360, {27,91,70}, {27,91,52,126}}
    CONTROL_HOME = {CONTROL_T, {27,91,49,59,53,72}, {27,91,49,126}} -- (top)
    CONTROL_END = {CONTROL_B, {27,91,49,59,53,70}, {27,91,52,126}} -- (bottom)
    PAGE_UP = {339, {27,91,53,126}}
    PAGE_DOWN = {338, {27,91,54,126}}
    INSERT = {331, {27,91,50,126}}
    DELETE = {330, {27,91,51,126}}
    XDELETE = {-999} -- 127 in xterm (not standard across terminals)
    ARROW_LEFT = {260, {27,91,68}}
    ARROW_RIGHT = {261, {27,91,67}}
    CONTROL_ARROW_LEFT = {CONTROL_L, {27,91,49,59,53,68}} -- (left)
    CONTROL_ARROW_RIGHT = {CONTROL_R, {27,91,49,59,53,67}} -- (right)
    ARROW_UP = {259, {27,91,65}}
    ARROW_DOWN = {258, {27,91,66}}
    F1 = {265, {27,79,80}, {27,91,91,65}}
    F2 = {266, {27,79,81}, {27,91,91,66}}
    F3 = {267, {27,79,82}, {27,91,91,67}}
    F4 = {268, {27,79,83}, {27,91,91,68}}
    F5 = {269, {27,91,49,53,126}, {27,91,91,69}}
    F6 = {270, {27,91,49,55,126}}
    F7 = {271, {27,91,49,56,126}}
    F8 = {272, {27,91,49,57,126}}
    F9 = {273, {27,91,50,48,126}}
    F10 = {274, {27,91,50,49,126}}
    F11 = {275, {27,91,50,51,126}}
    F12 = {276, {27,91,50,52,126}}

else
    -- DOS/Windows
--  SLASH = '\\'
    SAFE_CHAR = 14
--  delete_cmd = "del "
    compare_cmd = "fc /T "
    ESCAPE = 27
    CR = 13
    BS = 8
    CONTROL_DELETE = 403 -- key for line-delete
			 -- (not available on some systems)

    -- These are sequence for compatibility with Linux various terminals...
    -- * Use 'euphoria/bin/xkey.ex' TO ADD MORE SEQUENCES FOR YOUR SYSTEM.
    -- * See bookmark: Keys for Linux/FreeBSD terminals; above.
    HOME = {327}
    END = {335}
    CONTROL_HOME = {375}
    CONTROL_END = {373}
    PAGE_UP = {329}
    PAGE_DOWN = {337}
    INSERT = {338}
    DELETE = {339}
    XDELETE = {-999} -- never
    ARROW_LEFT = {331}
    ARROW_RIGHT = {333}
    CONTROL_ARROW_LEFT = {371}
    CONTROL_ARROW_RIGHT = {372}
    ARROW_UP = {328}
    ARROW_DOWN = {336}
    F1 = {315}
    F2 = {316}
    F3 = {317}
    F4 = {318}
    F5 = {319}
    F6 = {320}
    F7 = {321}
    F8 = {322}
    F9 = {323}
    F10 = {324}
    if OS_WIN32 then
	F11 = {343}
	F12 = {344}
    else
	F11 = {389}
	F12 = {390}
    end if
end if



-------- START OF USER-MODIFIABLE PARAMETERS ----------------------------------

-- make your own specialized macro command(s):
constant CUSTOM_KEY = F12[1]
constant CUSTOM_KEYSTROKES = { -- now it's bound to 'file_type' (Shian)
	HOME[1] & "# " & ARROW_DOWN[1],  -- file_type = FT_NONE [1]
	HOME[1] & "-- " & ARROW_DOWN[1], -- file_type = FT_EUPHORIA [2]
	HOME[1] & "// " & ARROW_DOWN[1], -- file_type = FT_C [3]
	HOME[1] & "' " & ARROW_DOWN[1]}  -- file_type = FT_QBASIC [4]

constant PROG_INDENT = 4  -- tab width for editing program source files
			  -- (tab width is 8 for other files)
-- Euphoria files:
constant E_FILES = {".e", ".ex", ".ew", ".exw", ".eu", ".exu", ".pro", ".cgi"}
constant C_FILES = {".c", ".h", ".cpp"} -- syntax color for C (Shian)
constant B_FILES = {".BAS", ".BI"} -- syntax color for MS-QBASIC (Shian)
-- program indent files:
constant PROG_FILES = E_FILES & C_FILES & B_FILES

constant WANT_COLOR_SYNTAX  = TRUE -- FALSE if you don't want
				   -- color syntax highlighting

constant WANT_AUTO_COMPLETE = TRUE -- FALSE if you don't want
				   -- auto-completion of Euphoria statements

constant WANT_BEEP_SOUND = TRUE -- FALSE if you don't want beep sound (DOS32)

constant WANT_LONG_FILE_NAMES = TRUE -- FALSE if you don't want LFN (DOS32)

constant HOT_KEYS = TRUE  -- FALSE if you want to hit Enter after each command

-- Date-time format to insert date-time by sprintd(), datetime.e, Lib2:
constant LONG_DATE_FORMAT = "%dddd, %mmmm/%d/%yyyy  %h:%tt"
constant SHORT_DATE_FORMAT = "%m/%d/%yyyy %h:%tt:%ss"

-- cursor style:
constant ED_CURSOR = THICK_UNDERLINE_CURSOR
		     -- UNDERLINE_CURSOR
		     -- HALF_BLOCK_CURSOR
		     -- BLOCK_CURSOR

-- number of lines on screen: (25,28,43 or 50)
constant INITIAL_LINES = 25,  -- when editor starts up
	 FINAL_LINES = 25     -- when editor is finished

-- theme colors: 1=edu (default), 2=ed, 3=blue, 4=mono (B&W), 5=user.
-- Adjust 'user' colors (index 5) to suit your monitor and your taste.
constant THEME_INDEX = 1    -- (1 to 5)

-- theme colors            {edu,   ed,    blue,  mono,  user}
constant C_TOP_LINE_TEXT = {BLACK, BLACK, BLACK, BLACK, BLACK},
	 C_TOP_LINE_BACK = {WHITE, BROWN, CYAN,  WHITE, WHITE},
	 C_TOP_LINE_DIM  = {BLUE,  BLUE,  BLUE,  GRAY , BLUE},
	 C_BACKGROUND    = {BLACK, WHITE, BLUE,  BLACK, CYAN},
	 -- status line critical-error colors
	 C_ERROR_TEXT    = {YELLOW,WHITE,YELLOW, BLACK+BLINKING, YELLOW},
	 C_ERROR_BACK    = {RED,   RED,  RED,    WHITE,  RED}
	 

-- theme colors (needed by sycoloru.e)
constant    --  {edu,           ed,     blue,          mono,         user}
    C_NORMAL  = {WHITE,         BLACK,  WHITE,         WHITE,        BLACK},
    C_NORMAL2 = C_NORMAL[THEME_INDEX],
    C_COMMENT = {CYAN,          RED,    CYAN,          GRAY,         BLUE},
    C_KEYWORD = {BRIGHT_WHITE,  BLUE,   BRIGHT_WHITE,  BRIGHT_WHITE, BRIGHT_WHITE},
    C_BUILTIN = {BRIGHT_MAGENTA,MAGENTA,BRIGHT_MAGENTA,BRIGHT_WHITE, RED},
    C_STRING  = {BRIGHT_CYAN,   GREEN,  BRIGHT_CYAN,   WHITE,        BRIGHT_CYAN},
    C_LIBRARY = {BRIGHT_GREEN,  BROWN,  BRIGHT_GREEN,  BRIGHT_WHITE, BRIGHT_GREEN},
    C_USER    = {YELLOW,        BROWN,  YELLOW,        BRIGHT_WHITE, YELLOW},
    C_BRACKET = {
	-- edu [1] (default)
	{C_NORMAL2, BRIGHT_WHITE, BRIGHT_CYAN, BRIGHT_GREEN,
			    YELLOW, BRIGHT_MAGENTA, BRIGHT_RED},
	-- ed [2]
	{C_NORMAL2, YELLOW, BRIGHT_WHITE, BRIGHT_BLUE,
			    BRIGHT_RED, BRIGHT_CYAN, BRIGHT_GREEN},
	-- blue [3]
	{C_NORMAL2, BRIGHT_WHITE, BRIGHT_CYAN, BRIGHT_GREEN,
			    YELLOW, BRIGHT_MAGENTA, BRIGHT_RED},
	-- mono [4]
	{C_NORMAL2, GRAY, BRIGHT_WHITE, C_NORMAL2, GRAY,
			    BRIGHT_WHITE, C_NORMAL2, GRAY},
	-- user [5]
	{C_NORMAL2, BRIGHT_WHITE, BRIGHT_CYAN, BRIGHT_GREEN,
			    YELLOW, BRIGHT_MAGENTA, BRIGHT_RED}
    }

-- number of characters to shift left<->right when you move beyond column 80
constant SHIFT = 4   -- 1..78 should be ok

-- name of edit buffer temp file for Esc m command
constant TEMPFILE = "editbuff.tmp"

-------- END OF USER-MODIFIABLE PARAMETERS ------------------------------------

allow_lfn(WANT_LONG_FILE_NAMES = TRUE) -- use LFN on DOS32 if possible

allow_error(FALSE) -- let edu handle critical-errors on DOS32

-- beep sound
constant PLAY_BEEP = (WANT_BEEP_SOUND and OS_DOS32)

-- theme colors
constant TOP_LINE_TEXT_COLOR = C_TOP_LINE_TEXT[THEME_INDEX],
	 TOP_LINE_BACK_COLOR = C_TOP_LINE_BACK[THEME_INDEX],
	 TOP_LINE_DIM_COLOR  = C_TOP_LINE_DIM[THEME_INDEX],
	 BACKGROUND_COLOR    = C_BACKGROUND[THEME_INDEX],
	 ERROR_TEXT_COLOR    = C_ERROR_TEXT[THEME_INDEX],
	 ERROR_BACK_COLOR    = C_ERROR_BACK[THEME_INDEX]

-- global theme colors needed by syncolor.e:
global constant NORMAL_COLOR  = C_NORMAL[THEME_INDEX],
		COMMENT_COLOR = C_COMMENT[THEME_INDEX],
		KEYWORD_COLOR = C_KEYWORD[THEME_INDEX],
		BUILTIN_COLOR = C_BUILTIN[THEME_INDEX],
		STRING_COLOR  = C_STRING[THEME_INDEX],
		LIBRARY_COLOR = C_LIBRARY[THEME_INDEX],
		USER_COLOR    = C_USER[THEME_INDEX],
		BRACKET_COLOR = C_BRACKET[THEME_INDEX]

-- Special keys that we can handle. Some are duplicates.
-- If you add more, you should list them here (for sequence keys remember to add
-- index [1]):
constant SPECIAL_KEYS = {ESCAPE, BS, DELETE[1], XDELETE[1], PAGE_UP[1], PAGE_DOWN[1],
			 CONTROL_P, CONTROL_U, CONTROL_T, CONTROL_B,
			 CONTROL_R, CONTROL_L, CONTROL_F, CONTROL_K,
			 MACRO_REC_KEY, MACRO_EXEC_KEY,
			 INSERT[1], CONTROL_DELETE,
			 CONTROL_D, ARROW_LEFT[1], ARROW_RIGHT[1], ARROW_UP[1],
			 ARROW_DOWN[1], CONTROL_ARROW_LEFT[1], CONTROL_ARROW_RIGHT[1],
			 HOME[1], END[1], CONTROL_HOME[1], CONTROL_END[1],
			 CAPS_LOCK, F1[1], F2[1], F3[1], F4[1], F5[1], F6[1],
			 F7[1], F8[1], F9[1], F10[1], CUSTOM_KEY}

-- output device:
constant SCREEN = 1

constant CONTROL_CHAR = 254 -- change funny control chars to this

constant STANDARD_TAB_WIDTH = 8

constant MAX_WINDOWS = 10 -- F1..F10

type natural(integer x)
    return x >= 0
end type

type positive_int(integer x)
    return x >= 1
end type

sequence buffer -- In-memory buffer where the file is manipulated.
-- This is a sequence where each element is a sequence
-- containing one line of text. Each line of text ends with '\n'

positive_int screen_length  -- number of lines on physical screen
positive_int screen_width   -- number of columns on screen - utf8_mode (Shian)

sequence BLANK_LINE

positive_int window_base    -- location of first line of current window
			    -- (status line)
window_base = 1
positive_int window_length  -- number of lines of text in current window

sequence window_list -- state info for all windows
window_list = {0}

sequence buffer_list -- all buffers
buffer_list = EMPTY

type window_id(integer x)
    return x >= 1 and x <= length(window_list)
end type

type buffer_id(integer x)
    return x >= 0 and x <= length(buffer_list)
end type

type window_line(integer x)
-- a valid line in the current window
    return x >= 1 and x <= window_length
end type

type screen_col(integer x)
-- a valid column on the screen
    return x >= 1
end type

type buffer_line(integer x)
-- a valid buffer line
    return (x >= 1 and x <= length(buffer)) or x = 1
end type

type char(integer x)
-- a character (including special key codes)
    return x >= 0 and x <= MAX_UNICODE -- ed.ex: x <= 511
end type

type extended_char(integer x)
    return char(x) or x = -1
end type

type file_number(integer x)
    return x >= -1
end type

string file_name   -- name of the file that we are editing (unicode-string)

-- These are the critical state variables that all editing operations
-- must update:
buffer_line  b_line  -- current line in buffer
positive_int b_col   -- current character within line in buffer
window_line  s_line  -- line on screen corresponding to b_line
screen_col   s_col   -- column on screen corresponding to b_col
natural s_shift      -- how much the screen has been shifted (for >80)
s_shift = 0

boolean stop         -- indicates when to stop processing current buffer

sequence kill_buffer -- kill buffer of deleted lines or characters
kill_buffer = EMPTY

boolean adding_to_kill  -- TRUE if still accumulating deleted lines/chars

boolean multi_color     -- use colors for keywords etc.
boolean auto_complete   -- perform auto completion of statements
-->----- boolean dot_e  -- now using 'file_type' from sycoloru.e (Shian)
boolean modified        -- TRUE if the file has been modified compared to
			-- what's on disk
boolean editbuff        -- TRUE if temp file exists (Esc m)
editbuff = FALSE

boolean freeze_cursor   -- TRUE if cursor don't move while typing (Shian),
freeze_cursor = FALSE   --  this allows Arabic,Hebrew,etc to be written in reverse.

atom buffer_version,    -- version of buffer contents
     my_buffer_version  -- last version used by current window

boolean control_chars,  -- binary file - view but don't save
	cr_removed      -- Linux: CR's were removed from DOS file (CR-LF)

natural start_line, start_col

string error_message

-- history are unicode-strings (not utf8-strings)
sequence file_history, command_history, search_history, replace_history
sequence search_dir_history, change_dir_history -- (Shian)
file_history = EMPTY
command_history = EMPTY
search_history = EMPTY
replace_history = EMPTY
search_dir_history = EMPTY
change_dir_history = EMPTY

sequence config -- video configuration

window_id window_number -- current active window
window_number = 1

buffer_id buffer_number -- current active buffer
buffer_number = 0

sequence key_queue -- queue of input characters forced by ed
key_queue = EMPTY

natural goto_help_line -- greater then 0 if using context sensitive help
goto_help_line = 0
string goto_help_dir -- if not "" then path of euphoria/doc dir (utf8 string)
goto_help_dir = ""

string new_file_dir -- new_file_from_list()'s last used directory (utf8 string)
new_file_dir = "." -- start in current dir

-- record and execute a single macro buffer (Shian)
sequence macro_buffer -- buffer for recorded macro
boolean macro_recording -- start/stop recording a macro
integer macro_execute -- key index in macro_buffer to execute
macro_buffer = EMPTY
macro_recording = FALSE
macro_execute = 0

boolean utf8_mode -- run edu.ex in ASCII (0) or UTF-8 (1) mode

procedure putsu(string s)
-- s is unicode-string.
-- putsu replaces wrap(0) and scroll() for supporting UTF-8 environment, and
-- for being compatible with ed.ex.
-- It prints unicode-string to the screen as utf8-string. In utf8_mode it does
-- not print on the last column of screen to avoid possible auto-scrolling.
    sequence pos
    integer overflow

    if utf8_mode then
	pos = get_position()
	if pos[2] <= screen_width then
	    overflow = screen_width-pos[2]+1-length(s)
	    if overflow < 0 then
		puts(SCREEN, unicode_to_utf8(s[1..$+overflow]))
	    else
		puts(SCREEN, unicode_to_utf8(s))
	    end if
	end if
    else -- ASCII mode, using wrap(0)
	puts(SCREEN, s)
    end if
end procedure

function maybe_utf8(string s)
-- unless speed is important, call maybe_utf8() instead of unicode_to_utf8(),
-- otherwise call unicode_to_utf8() in-line
    if utf8_mode then
	return unicode_to_utf8(s)
    else -- ASCII mode
	return s
    end if
end function

function maybe_unicode(string s)
-- unless speed is important, call maybe_unicode() instead of 
-- utf8_to_unicode(), otherwise call utf8_to_unicode() in-line
    if utf8_mode then
	return utf8_to_unicode(s)
    else -- ASCII mode
	return s
    end if
end function

procedure set_modified()
-- buffer differs from file
    modified = TRUE
    cursor(NO_CURSOR) -- hide cursor while we update the screen
    buffer_version += 1
end procedure

procedure clear_modified()
-- buffer is now same as file
    modified = FALSE
end procedure


natural edit_tab_width

function tab(natural tab_width, positive_int pos)
-- compute new column position after a tab
    return (floor((pos - 1) / tab_width) + 1) * tab_width + 1
end function

function expand_tabs(natural tab_width, string line)
-- replace tabs by blanks in a line of text
    natural tab_pos, column, ntabs

    column = 1
    while TRUE do
	tab_pos = find(TAB, line[column..$])
	if tab_pos = 0 then
	    -- no more tabs
	    return line
	else
	    tab_pos += column - 1
	end if
	column = tab(tab_width, tab_pos)
	ntabs = 1
	while line[tab_pos+ntabs] = TAB do
	    ntabs += 1
	    column += tab_width
	end while
	-- replace consecutive tabs by blanks
	line = line[1..tab_pos-1] & repeat(SPACE, column - tab_pos) &
		line[tab_pos+ntabs..$]
    end while
end function

function indent_tabs(natural tab_width, string line)
-- replace leading blanks of a line with tabs
    natural i, blanks

    if length(line) < tab_width then
	return line
    end if
    i = 1
    while line[i] = SPACE do
	i += 1
    end while
    blanks = i - 1
    return repeat(TAB, floor(blanks / tab_width)) &
	   BLANK_LINE[1..remainder(blanks, tab_width)] &
	   line[i..$]
end function

function convert_tabs(natural old_width, natural new_width, string line)
-- retabulate a line for a new tab size
    if old_width = new_width then
	return line
    end if
    return indent_tabs(new_width, expand_tabs(old_width, line))
end function

-- color display of lines (supports unicode-strings)
include sycoloru.e

procedure reverse_video()
-- start inverse video
    text_color(TOP_LINE_TEXT_COLOR)
    bk_color(TOP_LINE_BACK_COLOR)
end procedure

procedure normal_video()
-- end inverse video
    text_color(NORMAL_COLOR)
    bk_color(BACKGROUND_COLOR)
end procedure

procedure ClearLine(window_line sline)
-- clear the current line on screen
    scroll(1, window_base + sline, window_base + sline)
end procedure

procedure ClearWindow()
-- clear the current window
    scroll(window_length, window_base+1, window_base+window_length)
end procedure

procedure ScrollUp(positive_int top, positive_int bottom)
-- move text up one line on screen
   scroll(+1, window_base + top, window_base + bottom)
end procedure

procedure ScrollDown(positive_int top, positive_int bottom)
-- move text down one line on screen
    scroll(-1, window_base + top, window_base + bottom)
end procedure

procedure set_absolute_position(natural window_line, positive_int column)
-- move cursor to a line and an absolute (non-shifted) column within
-- the current window
    position(window_base + window_line, column)
end procedure

procedure DisplayLine(buffer_line bline, window_line sline, boolean all_clear)
-- display a buffer line on a given line on the screen
    sequence color_line
    string this_line, text
    natural last, last_pos, color, len

    this_line = expand_tabs(edit_tab_width, buffer[bline])
    last = length(this_line) - 1
    set_absolute_position(sline, 1)
    if multi_color then
	-- color display
	color_line = SyntaxColor(this_line)
	last_pos = 0

	for i = 1 to length(color_line) do
	    -- display the next colored text segment
	    color = color_line[i][1]
	    text = color_line[i][2]
	    len = length(text)
	    if last_pos >= s_shift then
		text_color(color)
		putsu(text)
	    elsif last_pos+len > s_shift then
		-- partly left-of-screen
		text_color(color)
		putsu(text[1+(s_shift-last_pos)..len])
		last_pos += len
	    else
		-- left-of-screen
		last_pos += len
	    end if
	end for
    else
	-- monochrome display
	if last > s_shift then
	    putsu(this_line[1+s_shift..last])
	end if
    end if
    if last-s_shift > screen_width then
	-- line extends beyond right margin
	set_absolute_position(sline, screen_width)
	text_color(BACKGROUND_COLOR)
	bk_color(NORMAL_COLOR)
	putsu({this_line[screen_width+s_shift]})
	normal_video()
    elsif not all_clear then
	putsu(BLANK_LINE)
    end if
end procedure

without warning
procedure DisplayWindow(positive_int bline, window_line sline)
-- print a series of buffer lines, starting at sline on screen
-- and continue until the end of screen, or end of buffer
    boolean all_clear

    if sline = 1 then -- note: might flicker in utf8_mode but necessary (Shian)
	ClearWindow()
	all_clear = TRUE
    else
	all_clear = FALSE
    end if

    for b = bline to length(buffer) do
	DisplayLine(b, sline, all_clear)
	if sline = window_length then
	    return
	else
	    sline += 1
	end if
    end for
    -- blank any remaining screen lines after end of file
    for s = sline to window_length do
	ClearLine(s)
    end for
end procedure
with warning

procedure set_position(natural window_line, positive_int column)
-- Move cursor to a logical screen position within the window.
-- The window will be shifted left<->right if necessary.
-- window_line 0 is status line, window_line 1 is first line of text
    natural s

    if window_line = 0 then
	-- status line
	position(window_base + window_line, column)
    else
	s = s_shift
	while column-s_shift > screen_width do
	    s_shift += SHIFT
	end while
	while column-s_shift < 1 do
	    s_shift -= SHIFT
	end while
	if s_shift != s then
	    -- display shifted window
	    DisplayWindow(b_line - s_line + 1, 1)
	end if
	position(window_base + window_line, column-s_shift)
    end if
end procedure

without warning
function clean(string line)
-- replace control characters with a graphics character
-- Linux: replace CR-LF with LF (for now) -- skip it (Shian)
    integer c

    for i = 1 to length(line) do
	c = line[i]
	if c < SAFE_CHAR and c != '\n' and c != TAB then
	    if c != '\r' or not OS_LINUX then
		line[i] = CONTROL_CHAR  -- replace with displayable character
		control_chars = TRUE
	    end if
	end if
    end for
    if line[$] != '\n' then
	line &= '\n'
    end if
    if OS_LINUX and
       length(line) > 1 and
       line[$ - 1] = '\r' then
       -- DOS file: remove CR
       cr_removed = TRUE
       line = line[1..$-2] & '\n'
    end if
    return line
end function
with warning

function add_line(file_number file_no)
-- add a new line to the buffer -- in memory we are using unicode-strings
    object line

    line = gets(file_no)

    if atom(line) then
	-- end of file
	return FALSE
    end if

    line = convert_tabs(STANDARD_TAB_WIDTH, edit_tab_width, clean(line))
    if utf8_mode then
	buffer = append(buffer, utf8_to_unicode(line))
    else -- ASCII mode
	buffer = append(buffer, line)
    end if
    return TRUE
end function

procedure new_buffer()
-- make room for a new (empty) buffer
    buffer_list &= 0 -- place holder for new buffer
    buffer_number = length(buffer_list)
    buffer = EMPTY
end procedure

procedure read_file(file_number file_no)
-- read the entire file into buffer variable

    -- read and immediately display the first screen-full
    for i = 1 to window_length do
	if not add_line(file_no) then
	    exit
	end if
    end for
    DisplayWindow(1, 1)

    -- read the rest
    while add_line(file_no) do
    end while

end procedure

procedure set_top_line(string message)
-- print message on top line
    set_position(0, 1)
    reverse_video()
    putsu(message & BLANK_LINE)
    set_position(0, length(message)+1)
end procedure

procedure set_status_line(string message)
-- update the status line (bottom line) when edit_file() (Shian)
-- if message is not empty, "", then just print a message on the status line.
    string cod, mac, eol
    integer ch, mo, fr
    sequence p

    if length(buffer) = 0 then
	ch = 0
    else
	ch = buffer[b_line][b_col]
    end if
    if utf8_mode then
	cod = sprintf("U+%06x", ch)
    else
	cod = sprintf("ASCII %03d", ch)
    end if
    if macro_recording then
	mac = "*macro(^w)"
    elsif length(macro_buffer) = 0 then
	mac = " macro(^w)"
    else
	mac = " macro(^o)"
    end if
    if modified then
	mo = 'M'
    else
	mo = '.'
    end if
    if freeze_cursor then
	fr = '<'
    else
	fr = SPACE
    end if
    if cr_removed or not OS_LINUX then -- (EOF mode)
	eol = "CRLF"
    else
	eol = "LF"
    end if
    p = get_position()
    cursor(NO_CURSOR)
    position(screen_length, 1)
    reverse_video()
    if check_error() then -- for DOS32
	bk_color(ERROR_BACK_COLOR)
	text_color(ERROR_TEXT_COLOR)
	putsu(sprintd(" [%h:%tt:%ss] Warning: critical-error has occurred!" & 
	    BLANK_LINE, now()))
    elsif length(message) then
	putsu(message)
    else -- edit file status
	putsu(sprintf(
	" EDU 3.20 <Esc> | tab: %d |%s: %-3d |%s| %05d:%03d%s| %s | %s |",
	    {edit_tab_width, mac, length(macro_buffer), mo,
	    b_line, s_col, fr, cod, eol}) & BLANK_LINE)
    end if
    normal_video()
    position(p[1], p[2]) -- restore cursor position
    cursor(ED_CURSOR)
end procedure

function cjk_char(integer ch)
-- In Linux Xterm terminal Chinese/Japanese/Korean characters require two
-- terminal character positions (double-width), so if ch is a CJK Unicode
-- code point then return 1, else return 0. (Shian)
    if utf8_mode then
	if  (ch >= #1100  and ch <= #11FF)  or -- Hangul Jamo
	    (ch >= #2E80  and ch <= #2FDF)  or -- CJK/Kangxi Radicals
	    (ch >= #2FF0  and ch <= #4DBF)  or -- CJK (few successive blocks)
	    (ch >= #4E00  and ch <= #A4CF)  or -- CJK Unified Ideographs & Yi
	    (ch >= #AC00  and ch <= #D7AF)  or -- Hangul Syllables
	    (ch >= #F900  and ch <= #FAFF)  or -- CJK Compatibility Ideographs
	    (ch >= #FE10  and ch <= #FE1F)  or -- Free block (double-width)
	    (ch >= #FE30  and ch <= #FE6F)  or -- CJK Compatibility/Small Form
	    (ch >= #FF00  and ch <= #FF60)  or -- Fullwidth Forms
	    (ch >= #FFE0  and ch <= #FFE6)  or -- Fullwidth Forms
	    (ch >= #20000 and ch <= #2A6DF) or -- CJK Extension B
	    (ch >= #2A700 and ch <= #2EBEF) or -- CJK Extensions C,D,E,F
	    (ch >= #2F800 and ch <= #2FA1F)    -- CJK Compatibility Ideographs
	then
	    return 1
	end if
    end if
    return 0
end function

procedure arrow_right()
-- action for right arrow key
    if b_col < length(buffer[b_line]) then
	if buffer[b_line][b_col] = TAB then
	    s_col = tab(edit_tab_width, s_col)
	else
	    s_col += 1 + cjk_char(buffer[b_line][b_col]) -->----- s_col += 1
	end if
	b_col += 1
    end if
end procedure

procedure arrow_left()
-- action for left arrow key

    positive_int old_b_col

    old_b_col = b_col
    b_col = 1
    s_col = 1
    for i = 1 to old_b_col - 2 do
	arrow_right()
    end for
end procedure

procedure skip_white()
-- set cursor to first non-whitespace in line
    positive_int temp_col

    while find(buffer[b_line][b_col], " \t") do
	temp_col = s_col
	arrow_right()
	if s_col = temp_col then
	    return -- can't move any further right
	end if
    end while
end procedure

procedure goto_line(integer new_line, integer new_col)
-- move to a specified line and column
-- refresh screen if line is 0
    integer new_s_line
    boolean refresh

    if length(buffer) = 0 then
	ClearWindow()
	s_line = 1
	s_col = 1
	return
    end if
    if new_line = 0 then
	new_line = b_line
	refresh = TRUE
    else
	refresh = FALSE
    end if
    if new_line < 1 then
	new_line = 1
    elsif new_line > length(buffer) then
	new_line = length(buffer)
    end if
    new_s_line = new_line - b_line + s_line
    b_line = new_line
    if not refresh and window_line(new_s_line) then
	-- new line is on the screen
	s_line = new_s_line
    else
	-- new line is off the screen, or refreshing
	s_line = floor((window_length+1)/2)
	if s_line > b_line or length(buffer) < window_length then
	    s_line = b_line
	elsif b_line > length(buffer) - window_length + s_line then
	    s_line = window_length - (length(buffer) - b_line)
	end if
	DisplayWindow(b_line - s_line + 1, 1)
    end if
    b_col = 1
    s_col = 1
    for i = 1 to new_col-1 do
	arrow_right()
    end for
    set_position(s_line, s_col)
end procedure

function plain_text(char c)
-- defines text for next_word, previous_word, and current word (Shian)
    return (c >= '0' and c <= '9') or
	   (c >= 'A' and c <= 'Z') or
	   (c >= 'a' and c <= 'z') or
	   c = '_' or
	   c > 127 -- Unicode or extended (ASCII) character
end function

function get_current_word(boolean get_pos)
-- get the word under cursor. if not a word then return "" (Shian)
-- if get_pos = TRUE return {start, end} of word (not the string itself).
    char c
    positive_int st, en

    if length(buffer) = 0 then
	return ""
    end if
    -- not a word-character?
    c = buffer[b_line][b_col]
    if not plain_text(c) then
	return ""
    end if
    -- get start of word
    st = b_col
    while st > 1 do
	c = buffer[b_line][st-1]
	if not plain_text(c) then
	    exit
	end if
	st -= 1
    end while
    -- get end of word (last char is always \n)
    en = b_col
    while TRUE do
	c = buffer[b_line][en+1]
	if not plain_text(c) then
	    exit
	end if
	en += 1
    end while
    if get_pos then
	return {st, en}
    else
	return buffer[b_line][st..en]
    end if
end function

procedure next_word()
-- move to start of next word in line
    char c
    positive_int col

    -- skip plain text
    col = b_col
    while TRUE do
	c = buffer[b_line][col]
	if not plain_text(c) then
	    exit
	end if
	col += 1
    end while

    -- skip white-space and punctuation
    while c != '\n' do
	c = buffer[b_line][col]
	if plain_text(c) then
	    exit
	end if
	col += 1
    end while
    goto_line(b_line, col)
end procedure

procedure previous_word()
-- move to start of previous word in line
    char c
    natural col

    -- skip white-space & punctuation
    col = b_col - 1
    while col > 1 do
	c = buffer[b_line][col]
	if plain_text(c) then
	    exit
	end if
	col -= 1
    end while

    -- skip plain text
    while col > 1 do
	c = buffer[b_line][col-1]
	if not plain_text(c) then
	    exit
	end if
	col -= 1
    end while

    goto_line(b_line, col)
end procedure

procedure arrow_up()
-- action for up arrow key

    b_col = 1
    s_col = 1
    if b_line > 1 then
	b_line -= 1
	if s_line > 1 then
	    s_line -= 1
	else
	    -- move all lines down, display new line at top
	    if utf8_mode then
		DisplayWindow(b_line - s_line + 1, 1)
	    else -- ASCII mode
		ScrollDown(1, window_length)
		DisplayLine(b_line, 1, TRUE)
	    end if
	    set_position(1, 1)
	end if
	skip_white()
    end if
end procedure

procedure arrow_down()
-- action for down arrow key
    b_col = 1
    s_col = 1
    if b_line < length(buffer) then
	b_line += 1
	if s_line < window_length then
	    s_line += 1
	else
	    -- move all lines up, display new line at bottom
	    if utf8_mode then
		DisplayWindow(b_line - s_line + 1, 1)
	    else -- ASCII mode
		ScrollUp(1, window_length)
		DisplayLine(b_line, window_length, TRUE)
	    end if
	end if
	skip_white()
    end if
end procedure

function numeric(string s)
-- convert digit string to an integer
    atom n

    n = 0
    for i = 1 to length(s) do
	if s[i] >= '0' and s[i] <= '9' then
	    n = n * 10 + s[i] - '0'
	    if not integer(n) then
		return 0
	    end if
	else
	    exit
	end if
    end for
    return n
end function

procedure page_down()
-- action for page-down key
    buffer_line prev_b_line

    if length(buffer) <= window_length then
	return
    end if
    prev_b_line = b_line
    b_col = 1
    s_col = 1
    if b_line + window_length + window_length - s_line <= length(buffer) then
	b_line = b_line + window_length
    else
	b_line = length(buffer) - (window_length - s_line)
    end if
    if b_line != prev_b_line then
	DisplayWindow(b_line - s_line + 1, 1)
    end if
end procedure

procedure page_up()
-- action for page-up key
    buffer_line prev_b_line

    if length(buffer) <= window_length then
	return
    end if
    prev_b_line = b_line
    b_col = 1
    s_col = 1
    if b_line - window_length >= s_line then
	b_line = b_line - window_length
    else
	b_line = s_line
    end if
    if b_line != prev_b_line then
	DisplayWindow(b_line - s_line + 1, 1)
    end if
end procedure

procedure set_f_line(natural w, string comment)
-- show F-key & file_name
    string f_key -->----- , text

    if length(window_list) = 1 then
	f_key = ""
    else -- print F1..F10
	f_key = sprintf("F%d/%d: ", w & length(window_list))
    end if
    set_top_line("")
    putsu(SPACE & f_key & file_name & comment)
-->----- text = "Esc for commands"
-->----- set_position(0, screen_width - length(text))
-->----- putsu(text)
end procedure

constant W_BUFFER_NUMBER = 1,
	 W_MY_BUFFER_VERSION = 2,
	 W_WINDOW_BASE = 3,
	 W_WINDOW_LENGTH = 4,
	 W_B_LINE = 11

procedure save_state()
-- save current state variables for a window
    window_list[window_number] = {buffer_number, buffer_version, window_base,
				  window_length, auto_complete, multi_color,
			       file_type, control_chars, cr_removed, file_name,
				  b_line, b_col, s_line, s_col, s_shift,
				  edit_tab_width}
    buffer_list[buffer_number] = {buffer, modified, buffer_version}
end procedure

procedure restore_state(window_id w)
-- restore state variables for a window
    sequence state
    sequence buffer_info

    -- set up new buffer
    state = window_list[w]
    window_number = w
    buffer_number = state[W_BUFFER_NUMBER]
    buffer_info = buffer_list[buffer_number]
    buffer = buffer_info[1]
    modified = buffer_info[2]
    buffer_version = buffer_info[3]
    buffer_list[buffer_number] = 0 -- save space

    -- restore other variables
    my_buffer_version = state[2]
    window_base = state[3]
    window_length = state[4]
    auto_complete = state[5]
    multi_color = state[6]
    file_type = state[7]
    control_chars = state[8]
    cr_removed = state[9]
    file_name = state[10]
    edit_tab_width = state[16]
    if multi_color then
	init_class() -- support different syntax coloring (Shian)
    end if
    set_f_line(w, "")
    normal_video()
    if buffer_version != my_buffer_version then
	-- buffer has changed since we were last in this window
	-- or window size has changed
	if state[W_B_LINE] > length(buffer) then
	    if length(buffer) = 0 then
		b_line = 1
	    else
		b_line = length(buffer)
	    end if
	else
	    b_line = state[W_B_LINE]
	end if
	s_shift = 0
	goto_line(0, 1)
    else
	b_line = state[W_B_LINE]
	b_col = state[12]
	s_line = state[13]
	s_col = state[14]
	s_shift = state[15]
	set_position(s_line, s_col)
    end if
end procedure

-- note: edu.ex is using full-screen window (Shian)

-->----- procedure refresh_other_windows(positive_int w)
-->----- -- redisplay all windows except w
-->-----
-->-----    normal_video()
-->-----    for i = 1 to length(window_list) do
-->-----    if i != w then
-->-----            restore_state(i)
-->-----            set_f_line(i, "")
-->-----            normal_video()
-->-----            goto_line(0, b_col)
-->-----            save_state()
-->-----        end if
-->-----    end for
-->----- end procedure

procedure set_window_size()
-- note: edu.ex is using full-screen window (Shian)
-- set sizes for windows
-->-----    natural nwindows, lines, base, size

-->-----    nwindows = length(window_list)
-->-----    lines = screen_length - nwindows
-->-----    base = 1
    for i = 1 to length(window_list) do
-->-----        size = floor(lines / nwindows)
	window_list[i][W_WINDOW_BASE] = 1 -->----- base
	window_list[i][W_WINDOW_LENGTH] = screen_length - 2 -->----- size
	window_list[i][W_MY_BUFFER_VERSION] = -1 -- force redisplay
-->-----        base += size + 1
-->-----        nwindows -= 1
-->-----        lines -= size
    end for
end procedure

procedure clone_window()
-- set up a new window that is a clone of the current window
-- save state of current window
    window_id w

    if length(window_list) >= MAX_WINDOWS then
	return
    end if
    save_state()
    -- create a place for new window
    window_list = window_list[1..window_number] &
		  {window_list[window_number]} &  -- the new clone window
		  window_list[window_number+1..$]
    w = window_number + 1
    set_window_size()
-->----- refresh_other_windows(w)
    restore_state(w)
end procedure

procedure switch_window(integer new_window_number)
-- switch context to a new window on the screen

    if new_window_number != window_number then
	save_state()
	restore_state(new_window_number)
    end if
end procedure

function delete_window()
-- delete the current window
    boolean buff_in_use

    buffer_list[buffer_number] = {buffer, modified, buffer_version}
    window_list = window_list[1..window_number-1] &
		  window_list[window_number+1..$]
    buff_in_use = FALSE
    for i = 1 to length(window_list) do
	if window_list[i][W_BUFFER_NUMBER] = buffer_number then
	    buff_in_use = TRUE
	    exit
	end if
    end for
    if not buff_in_use then
	buffer_list[buffer_number] = 0 -- discard the buffer
    end if
    if length(window_list) = 0 then
	return TRUE
    end if
    set_window_size()
-->----- refresh_other_windows(1)
    window_number = 1
    restore_state(window_number)
    set_position(s_line, s_col)
    return FALSE
end function

procedure add_queue(sequence keystrokes)
-- add to artificial queue of keystrokes
    key_queue &= keystrokes
end procedure

function next_key()
-- return the next key or unicode-character from the user, or
-- from our artificial queue of keystrokes. Check for control-c.
-- Always returns integer.
    object c
    extended_char key

    if macro_execute then
	-- read next macro-keystroke (ascii/unicode or other key)
	key = macro_buffer[macro_execute]
	macro_execute -= 1

    elsif length(key_queue) then
	-- read next artificial keystroke
	key = key_queue[1]
	key_queue = key_queue[2..$]

    else
	-- read a new keystroke(s) from user (returns integer or sequence of
	-- keystrokes on Linux, using wait_xkey())
	c = wait_xkey()

	if check_break() then
	    key = CONTROL_C

	elsif atom(c) then
	    -- single-byte ASCII/UTF-8 character "abc"
	    if c <= 127 or not utf8_mode then
		key = c
	    elsif find(c, SPECIAL_KEYS) then
		key = c
	    else
		-- characters 128-255 are not supported by UTF-8
		key = -1
	    end if

	elsif sequence(c) then
	    if c[1] = ESCAPE then
		-- process escape sequence
		   if find(c, HOME) then            c = HOME
		elsif find(c, END) then             c = END
		elsif find(c, CONTROL_HOME) then    c = CONTROL_HOME
		elsif find(c, CONTROL_END) then     c = CONTROL_END
		elsif find(c, PAGE_UP) then         c = PAGE_UP
		elsif find(c, PAGE_DOWN) then       c = PAGE_DOWN
		elsif find(c, INSERT) then          c = INSERT
		elsif find(c, DELETE) then          c = DELETE
		elsif find(c, XDELETE) then         c = XDELETE
		elsif find(c, ARROW_LEFT) then      c = ARROW_LEFT
		elsif find(c, ARROW_RIGHT) then     c = ARROW_RIGHT
		elsif find(c, CONTROL_ARROW_LEFT) then  c = CONTROL_ARROW_LEFT
		elsif find(c, CONTROL_ARROW_RIGHT) then c = CONTROL_ARROW_RIGHT
		elsif find(c, ARROW_UP) then        c = ARROW_UP
		elsif find(c, ARROW_DOWN) then      c = ARROW_DOWN
		elsif find(c, F1) then  c = F1
		elsif find(c, F2) then  c = F2
		elsif find(c, F3) then  c = F3
		elsif find(c, F4) then  c = F4
		elsif find(c, F5) then  c = F5
		elsif find(c, F6) then  c = F6
		elsif find(c, F7) then  c = F7
		elsif find(c, F8) then  c = F8
		elsif find(c, F9) then  c = F9
		elsif find(c, F10) then c = F10
		elsif find(c, F11) then c = F11
		elsif find(c, F12) then c = F12
		else -- key is not defined
		    c = {ESCAPE}
		end if

		key = c[1] -- return integer

	    else
		-- convert UTF-8 string (bytes from 128 to 255) into
		-- a single Unicode code point in the range 128 to 1114111.
		-- (allows copy/paste unicode-string with mouse in Linux Xterm).
		if utf8_mode then
		    c = utf8_to_unicode(c) -- on error returns UNICODE_REP.
		end if
		if length(c) > 1 then
		    add_queue(c[2..$]) -- just in case!
		end if
		key = c[1]

	    end if
	end if
    end if

    -- add key to macro buffer if needed (Shian)
    if macro_recording and key != MACRO_EXEC_KEY and key != MACRO_REC_KEY then
	macro_buffer = prepend(macro_buffer, key)
    end if
    return key
end function

procedure refresh_all_windows()
-- redisplay all the windows
    window_id w

    w = window_number
    save_state()
-->----- refresh_other_windows(w)
    restore_state(w)
end procedure

function key_gets(string hot_keys, sequence history)
-- Return an input unicode-string (not utf8-string) from the keyboard.
-- Handles special editing keys.
-- Some keys are "hot" - no Enter required (hot keys are not case sensitive, Shian).
-- A list of "history" unicode-strings (not utf8-strings) can be supplied,
-- and accessed by the user using up/down arrow.
    string input_string
    integer line, init_column, column, char, col, h
    sequence cursor
    boolean first_key

    if not HOT_KEYS then
	hot_keys = ""
    else
	-- ignore case for hot_keys, get & return *only* lower-case (Shian)
	hot_keys = lower(hot_keys)
    end if
    cursor = get_position()
    line = cursor[1]
    init_column = cursor[2]
    if init_column >= screen_width then -- bug fix to avoid crash (Shian)
	init_column = screen_width-1
    end if
    history = append(history, "")
    h = length(history)
    if h > 1 then
       h -= 1  -- start by offering the last choice
    end if
    input_string = history[h]
    column = init_column
    first_key = TRUE

    while TRUE do
	position(line, init_column)
	putsu(input_string)
	putsu(BLANK_LINE)
	if column >= screen_width then -- bug fix to avoid crash (Shian)
	    position(line, screen_width-1)
	else
	    position(line, column)
	end if

	char = next_key()

	if char = ESCAPE then -- List-box or Cancel command (Shian)
	    input_string = ""
	    exit

	elsif char = CR or char = 10 then
	    exit

	elsif char = BS then
	    if column > init_column then
		column -= 1
		col = column-init_column
		input_string = input_string[1..col] & input_string[col+2..$]
	    end if

	elsif char = ARROW_LEFT[1] then
	    if column > init_column then
		column -= 1
	    end if

	elsif char = ARROW_RIGHT[1] then
	    if column < init_column+length(input_string) and
	       column < screen_width then
		column += 1
	    end if

	elsif char = ARROW_UP[1] then
	    if h > 1 then
		h -= 1
	    else
		h = length(history)
	    end if
	    input_string = history[h]
	    column = init_column + length(input_string)

	elsif char = ARROW_DOWN[1] then
	    if h < length(history) then
		h += 1
	    else
		h = 1
	    end if
	    input_string = history[h]
	    column = init_column + length(input_string)

	elsif char = DELETE[1] or char = XDELETE[1] then
	    if column - init_column < length(input_string) then
		col = column-init_column
		input_string = input_string[1..col] & input_string[col+2..$]
	    end if

	elsif char = HOME[1] then
	    column = init_column

	elsif char = END[1] then
	    column = init_column+length(input_string)

	elsif (utf8_mode and char >= 32 and char <= MAX_UNICODE) or
	    (char >= 32 and char <= 255) or --(some PCs return >255 on DOS!)
	    char = TAB
	then
	    -- normal key
	    if first_key then
		input_string = ""
	    end if
	    if column < screen_width then
		if char = TAB then
		    char = SPACE
		end if
		column += 1
		if column - init_column > length(input_string) then
		    input_string = append(input_string, char)
		    if column = init_column + 1 and find(lower(char), hot_keys) then
			-- ignore case for hot_keys, use only lower-case (Shian)
			input_string = lower(input_string)
			exit
		    end if
		else
		    col = column-init_column
		    input_string = input_string[1..col-1] & char &
				    input_string[col..$]
		end if
	    end if

	elsif char = CONTROL_C then
	    -- refresh screen, treat as Enter key
	    refresh_all_windows()
	    goto_line(0, b_col)
	    input_string &= CR
	    exit
	end if

	first_key = FALSE
    end while

    return input_string
end function

procedure short_beep()
-- play a short beep
    if PLAY_BEEP then
	tick_rate(100)
	sound(300)
	delay(0.1)
	sound(0)
	tick_rate(0)
    end if  
end procedure

procedure new_screen_length()
-- set new number of lines on screen
    natural nlines
    window_id w

    set_top_line("How many lines on screen? (25, 28, 43, 50) ")
    nlines = numeric(key_gets("", EMPTY))
    if nlines then
	screen_length = text_rows(nlines)
	if screen_length != nlines then
	    short_beep() -- cannot set new lines
	end if
	w = window_number
	save_state()
	set_window_size()
-->-----        refresh_other_windows(w)
	restore_state(w)
    end if
end procedure


-- searching/replacing variables
boolean searching, replacing, match_case
boolean not_whole_word -- (Shian)
searching = FALSE
replacing = FALSE
match_case = TRUE
not_whole_word = TRUE

string find_string -- current (default) string to look for
find_string = ""

string replace_string -- current (default) string to replace with
replace_string = ""

procedure replace()
-- replace find_string by replace_string
-- we are currently positioned at the start of an occurrence of find_string
    string line

    set_modified()
    line = buffer[b_line]
    line = line[1..b_col-1] & replace_string &
	    line[b_col+length(find_string)..$]
    buffer[b_line] = line
    -- position at end of replacement string
    for i = 1 to length(replace_string)-1 do
	arrow_right()
    end for
    DisplayLine(b_line, s_line, FALSE)
end procedure

function alphabetic(object s)
-- does s contain alphabetic characters?
    return find(TRUE, (s >= 'A' and s <= 'Z') or
		      (s >= 'a' and s <= 'z'))
end function

function case_match(string s, sequence text)
-- Find string s in text with
-- 1. either case-sensitive or non-case-sensitive comparison
-- 2. either whole-word-only or non-whole-word comparison (Shian)
    integer pos

    pos = 0
    while TRUE do
	if match_case then
	    pos = match_from(s, text, pos + 1)
	else -- any case
	    pos = match_from(lower(s), lower(text), pos + 1)
	end if
	if pos = 0 then
	    return 0
	elsif not_whole_word then
	    return pos
	elsif pos > 1 and plain_text(text[pos-1]) then
	    -- continue search same line
	elsif pos + length(s) <= length(text) and
	    plain_text(text[pos + length(s)])
	then
	    -- continue search same line
	else
	    return pos -- it's a whole word
	end if
    end while
end function

function update_history(sequence history, string s)
-- update a history variable - string s will be placed at the end
    integer f

    f = find(s, history)
    if f then
	-- delete it
	history = history[1..f-1] & history[f+1..$]
    end if
    -- put it at the end
    return append(history, s)
end function

function list_box(string msg, sequence list, integer index)
-- general purpose full-screen list-box. return index in list if user press
-- Enter, 0 if user press Escape, or -1 if user press BackSpace. (Shian)
    integer len, top, key

    len = length(list)
    top = 1 -- index of first item on screen
    if index < 1 or index > length(list) then
	index = 1 -- index of current item in list
    end if
    set_status_line(
	" <Up/Down> <PgUp/PgDn> <Home/End> <BkSp=Back> <Esc=Cancel> <Enter=OK>" &
	BLANK_LINE)
    set_top_line("")
    if len = 0 then
	putsu("empty list. press any key...")
	pause()
	normal_video()
	return 0 -- Escape
    else
	putsu(msg)
	normal_video()
    end if
    while TRUE do
	if index > top+window_length-1 then
	    top = index-window_length+1
	elsif index < top then
	    top = index
	end if
	-- print one page on screen (in utf8_mode we cannot use scroll()).
	for i = 0 to window_length-1 do
	    position(2+i, 1)
	    if top+i <= len then
		if top+i = index then
		    reverse_video()
		    putsu(list[top+i])
		    normal_video()
		else
		    putsu(list[top+i])
		end if
	    end if
	    putsu(BLANK_LINE)
	end for
	position(2+index-top, 1) -- set blinking cursor position
	while TRUE do
	    key = next_key()
	    if key = ARROW_DOWN[1] and index < len then
		index += 1
		exit
	    elsif key = ARROW_UP[1] and index > 1 then
		index -= 1
		exit
	    elsif key = PAGE_DOWN[1] and index < len then
		if index+window_length <= len then
		    index += window_length
		else
		    index = len
		end if
		exit
	    elsif key = PAGE_UP[1] and index > 1 then
		if index-window_length >= 1 then
		    index -= window_length
		else
		    index = 1
		end if
		exit
	    elsif key = HOME[1] and index > 1 then
		index = 1
		exit
	    elsif key = END[1] and index < len then
		index = len
		exit
	    elsif key = CR then
		return index
	    elsif key = ESCAPE then
		return 0
	    elsif key = BS then
		return -1
	    end if
	end while
    end while
end function

procedure jump()
-- jump to next declaration. keyword must be on column 1. (Shian)
    sequence words, list
    string tmp
    integer line

    if file_type = FT_EUPHORIA then
	words = {
	"function", "procedure", "type", "global", "include", "with", "without",
	"public", "export", "override", -- eu4 global routines
	"--bookmark", "---", " ---" -- jump to code section
--      "constant", "integer", "atom", "sequence", "object",
--      "boolean", "string"
	}
    elsif file_type = FT_C then
	words = {
	"{", "/*", "//", "typedef", "struct", "#define", "#include", "#if",
	"void", "int", "static", "extern", "---", " ---"
	}
    elsif file_type = FT_QBASIC then
	words = {
	"FUNCTION", "SUB", "TYPE", "DEF", "COMMON", "SHARED", "DECLARE",
	"GOSUB", "GOTO", "OPTION", "REM", "'--", "' --", "'==", "' ==",
	"'$INCLUDE", "'$STATIC", "'$DYNAMIC", "'$FORM", "CHAIN", "RUN"
	}
    else -- file_type = FT_NONE: txt, bas, bat, bash, diff, etc
	words = {
	"-", " -", "=", " =", "*", " *", "#", "[", "'", "<", ";", "@",
	"rem", "REM", "func", "FUNC", "sub", "SUB", "proc", "PROC"
	}
    end if
    list = EMPTY
    line = 0
    -- create list of lines with declaration
    for b = 1 to length(buffer) do
	for w = 1 to length(words) do
	    if match(words[w], buffer[b]) = 1 then
		tmp = expand_tabs(edit_tab_width, buffer[b][1..$-1])
		list = append(list, sprintf("%05d: ", b) & tmp)
		if line = 0 then
		    -- set current index of list
		    if b = b_line then
			line = length(list) -- cursor is on declaration
		    elsif b > b_line and length(list) > 1 then
			line = length(list) - 1 -- cursor after declaration
		    end if
		end if
		exit
	    end if
	end for
    end for

    if line = 0 then
	line = length(list) -- set index for the last item in list
    end if
    line = list_box("jump to declaration:", list, line)
    normal_video()
    DisplayWindow(b_line - s_line + 1, 1) -- refresh full-screen window
    if line > 0 then
	tmp = value(list[line]) -- value() stops reading at ':'
	goto_line(tmp[2], 1)
    end if
end procedure

function search(boolean continue)
-- find a string from here to the end of the file
-- return TRUE if string is found
    natural col
    sequence pos
    string temp_string, answer

    set_top_line("")
    if length(buffer) = 0 then
	putsu("buffer empty")
	return FALSE
    end if
    putsu("searching for: ")
    if continue then
	putsu(find_string)
    else
	pos = get_position()
	temp_string = find_string
	find_string = key_gets("", search_history)
	if length(find_string) > 0 then
	    if not equal(temp_string, find_string) then
		-- new string typed in
		search_history = update_history(search_history, find_string)
		if length(find_string) < 40 then
		    col = pos[2]+length(find_string)+3
		    if alphabetic(find_string) then
			-- match case?
			set_position(0, col)
			putsu("match case [y,n]? n")
			pos = get_position()
			set_position(0, pos[2] - 1)
			answer = lower(key_gets("", EMPTY))
			if find('y', answer) then
			    match_case = TRUE
			elsif find('n', answer) then
			    match_case = FALSE
			else
			    normal_video()
			    return FALSE -- let me escape
			end if
		    end if
		    -- whole word?
		    set_position(0, col)
		    putsu("whole word [y,n]? n")
		    pos = get_position()
		    putsu(BLANK_LINE)
		    set_position(0, pos[2] - 1)
		    answer = lower(key_gets("", EMPTY))
		    if find('y', answer) then
			not_whole_word = FALSE
		    elsif find('n', answer) then
			not_whole_word = TRUE
		    else
			normal_video()
			return FALSE -- let me escape
		    end if
		end if
	    end if
	    if replacing then
		set_top_line("")
		putsu("replace with: ")
		replace_string = key_gets("", replace_history)
		replace_history = update_history(replace_history,
						 replace_string)
	    end if
	end if
    end if

    normal_video()
    if length(find_string) = 0 then
	return FALSE
    end if
    col = case_match(find_string, buffer[b_line][b_col+1..length(buffer[b_line])])
    if col then
	-- found it on this line after current position
	for i = 1 to col do
	    arrow_right()
	end for
	if replacing then
	    replace()
	end if
	return TRUE
    else
	-- check lines following this one
	for b = b_line+1 to length(buffer) do
	    col = case_match(find_string, buffer[b])
	    if col then
		goto_line(b, 1)
		for i = 1 to col - 1 do
		   arrow_right()
		end for
		if replacing then
		    replace()
		end if
		set_top_line("")
		putsu("searching for: " & find_string)
		return TRUE
	    end if
	end for
	set_top_line("")
	putsu('"' & find_string & "\" not found  (")
	if not_whole_word then
	    putsu("any word")
	else
	    putsu("whole word")
	end if
	if alphabetic(find_string) then
	    if match_case then
		putsu("; match case")
	    else
		putsu("; any case")
	    end if
	end if
	putsu(")")
    end if
    return FALSE
end function

procedure show_message()
-- display error message from ex.err
    if length(error_message) > 0 then
	set_top_line("")
	putsu(error_message)
	normal_video()
    end if
    set_position(s_line, s_col)
end procedure

procedure set_err_pointer()
-- set cursor at point of error

    for i = 1 to screen_width*5 do -- prevents infinite loop
	if s_col >= start_col then
	    exit
	end if
	arrow_right()
    end for
end procedure

function delete_trailing_white(string name)
-- get rid of blanks, tabs, newlines at end of string
    while length(name) > 0 do
	if find(name[$], "\n\r\t ") then
	    name = name[1..$-1]
	else
	    exit
	end if
    end while
    return name
end function

function get_err_line()
-- try to get file name & line number from ex.err
-- returns file_name, sets start_line, start_col, error_message

    file_number err_file
    string file_name -- file_name return as unicode-string (not utf8)
    sequence err_lines
    object temp_line
    natural colon_pos

    err_file = lfn_open("ex.err", "r")
    if err_file = -1 then
	error_message = ""
    else
	-- read the top of the ex.err error message file into unicode-strings
	err_lines = EMPTY
	while length(err_lines) < 6 do
	    temp_line = gets(err_file)
	    if atom(temp_line) then
		exit
	    end if
	    if utf8_mode then
		err_lines = append(err_lines, utf8_to_unicode(temp_line))
	    else -- ASCII mode
		err_lines = append(err_lines, temp_line)
	    end if
	end while
	close(err_file)
	-- look for file name, line, column and error message

	if length(err_lines) > 1 and match("TASK ID ", err_lines[1]) then
	    err_lines = err_lines[2..$]
	end if

	if length(err_lines) > 0 then
	    if string(err_lines[1]) then
		colon_pos = match(".e", lower(err_lines[1]))
		if colon_pos then
		    if find(err_lines[1][colon_pos+2], "xXwWuU") then
			colon_pos += 1
			if find(err_lines[1][colon_pos+2], "wWuU") then
			    colon_pos += 1
			end if
		    end if
		    file_name = err_lines[1][1..colon_pos+1]
		    start_line = numeric(err_lines[1][colon_pos+3..
							  length(err_lines[1])])
		    error_message = delete_trailing_white(err_lines[2])
		    if length(err_lines) > 3 then
			start_col = find('^', expand_tabs(STANDARD_TAB_WIDTH,
					      err_lines[$-1]))
		    end if
		    return file_name
		end if
	    end if
	end if
    end if
    return ""
end function

function last_use()
-- return TRUE if current buffer
-- is only referenced by the current window
    natural count

    count = 0
    for i = 1 to length(window_list) do
	if window_list[i][W_BUFFER_NUMBER] = buffer_number then
	    count += 1
	    if count = 2 then
		return FALSE
	    end if
	end if
    end for
    return TRUE
end function

procedure save_file(string save_name)
-- write buffer to the disk file
-- save_name is unicode-string (not utf8-string)
    file_number file_no
    boolean strip_cr
    string line

    if control_chars then
	set_top_line("")
	putsu(save_name & ": control chars were changed to " & CONTROL_CHAR &
		       " - save anyway? (y,n): ")
	if not find('y', key_gets("yn", EMPTY)) then
	    stop = FALSE
	    return
	end if
    end if
    strip_cr = FALSE -- keep CR-LF for DOS/Win file, so don't ask (Shian)
-->-----        if cr_removed then
-->-----            set_top_line("")
-->-----            putsu(save_name & ": Convert CR-LF (DOS-style) to LF (Linux-style)? (y,N): ")
-->-----            strip_cr = find('y', key_gets("yn", EMPTY))
-->-----        end if
    set_top_line("")
    file_no = lfn_open(maybe_utf8(save_name), "w")
    if file_no = -1 then
	putsu("Can't save " & save_name & " - write permission denied")
	stop = FALSE
	return
    end if
    putsu("saving " & save_name & "... ")
    for i = 1 to length(buffer) do
	line = buffer[i]
	if cr_removed and not strip_cr then
	    -- He wants CR's - put them back.
	    -- All lines have \n at the end.
	    if length(line) < 2 or line[$-1] != '\r' then
		line = line[1..$-1] & "\r\n"
	    end if
	end if
	line = convert_tabs(edit_tab_width, STANDARD_TAB_WIDTH, line)
	if utf8_mode then
	    puts(file_no, unicode_to_utf8(line))
	else
	    puts(file_no, line)
	end if
    end for
    close(file_no)
-->-----    if strip_cr then -- keep CR-LF for DOS/Win file (Shian)
-->-----        -- the file doesn't have CR's
-->-----        cr_removed = FALSE
-->-----    end if
    putsu("ok")
    if equal(save_name, file_name) then
	clear_modified()
    end if
    stop = TRUE
end procedure

procedure restore_graphics_mode(integer vc_status)
-- restore graphics mode after shell() only if necessary (fast/non-blinking)
-- vc_status is returned from dos_system()  
    integer ok
    
    if vc_status then
	ok = graphics_mode(3) -- 80x25
	if screen_length != 25 then
	    ok = text_rows(screen_length) 
	end if
    end if
    if not utf8_mode then 
	wrap(0) -- always restore wrap(0)
    end if
end procedure

procedure shell(string command)
-- run an external command
-- command is a unicode-string (not utf8-string)
    integer vc_status, ok
    sequence cdir
    
    bk_color(BLACK)
    text_color(WHITE)
    clear_screen()
--  tick_rate(0) -- restore system tick rate (currently not needed)
-->-----     system(maybe_utf8(command), 1) -- (with BEEP)
    cdir = current_dir() -- save current dir
    vc_status = dos_system(maybe_utf8(command)) -- (without BEEP)
    ok = chdir(cdir) -- restore current dir/drive after shell
    ok = chdrive(cdir[1])
    short_beep()
    pause()
    restore_graphics_mode(vc_status)
    normal_video()
    refresh_all_windows()
end procedure

procedure first_bold(string s)
-- highlight first char
    text_color(TOP_LINE_TEXT_COLOR)
    putsu({s[1]})
    text_color(TOP_LINE_DIM_COLOR)
    putsu(s[2..$])
end procedure

procedure delete_editbuff()
-- Shutting down. Delete EDITBUFF.TMP
    if editbuff then
	if delete_file(TEMPFILE) then -- system(delete_cmd & TEMPFILE, 2)
	end if
    end if
end procedure

function clone_new_self_command()
-- return sequence Esc-c (clone) & Esc-n (new) (Shian)
    if HOT_KEYS then
	return {ESCAPE, 'c', ESCAPE, 'n'}
    else
	return {ESCAPE, 'c', '\n', ESCAPE, 'n', '\n'}
    end if
end function

function get_help_index()
-- open bin\edu.hlp to get keyword-index table for control-K
-- return -1 on error, or keyword-index sequence
-- note: if edu.hlp is invalid then use bin\eduhelp.ex to create it again
    integer fn
    sequence s, chunk, index
    
    -- load index table from binary file
    fn = open(EUDIR & SLASH & "bin" & SLASH & "edu.hlp", "rb")
    if fn = -1 then
	return -1
    end if
    s = EMPTY
    while TRUE do
	chunk = get_bytes(fn, 100)
	s &= chunk
	if length(chunk) < 100 then
	    close(fn)
	    exit
	end if
    end while
    
    s = sencode(s, {1,2,3}) -- see bin\eduhelp.ex for details
    index = value(s)
    if index[1] = GET_SUCCESS and sequence(index[2]) then
	return index[2] -- should be ok
    end if
    return -1 -- edu.hlp is invalid
end function

-- = {{doc file name, {"word","word",...}, {line,line,...}}, ...}
constant EDU_HLP_INDEX = get_help_index()

procedure context_sensitive_help()
-- search doc files for "<current_word>---" (Shian)
    sequence index
    integer p
    string word, file, self_command

    if atom(EUDIR) then
	set_top_line(EUDIR_NOT_SET)
	normal_video()
	return -- Euphoria hasn't been installed yet
    elsif atom(EDU_HLP_INDEX) then
	set_top_line("'edu.hlp' is invalid - Exit & Run " & EUDIR & SLASH & 
	    "bin" & SLASH & "eduhlp.ex to fix it.")
	normal_video()
	return 
    end if

    self_command = clone_new_self_command()
    word = maybe_utf8(get_current_word(FALSE))

    if length(word) then
	for i = 1 to length(EDU_HLP_INDEX) do
	    -- index = {doc name, {"word","word",...}, {line,line,...}}
	    index = EDU_HLP_INDEX[i]
	    p = find(word, index[2])
	    if p then
		file = index[1]
		file = EUDIR & SLASH & "doc" & SLASH & file
		add_queue(self_command & file & CR)
		goto_help_line = index[3][p]
		return -- open doc file and goto line
	    end if
	end for
    end if
    
    -- keyword not found so open doc directory using new_file_from_list()
    add_queue(self_command & ESCAPE)
    goto_help_dir = EUDIR & SLASH & "doc" -- goto_help_dir is a utf8 string
end procedure

procedure new_file_from_list()
-- display a list-box for choosing new file name (Shian)
    sequence names
    string save_dir, path
    integer ok, index
    boolean help

    save_dir = lfn_current_dir()

    help = (length(goto_help_dir) > 0)
    if help then
	ok = lfn_chdir(goto_help_dir) -- (utf8 string)
	goto_help_dir = ""
    else
	ok = lfn_chdir(new_file_dir) -- open last used directory (utf8 string)
    end if
    while TRUE do
	names = lfn_dir(lfn_current_dir())
	for i = 1 to length(names) do
	    if find('d', names[i][D_ATTRIBUTES]) then
		names[i] = ">  " & maybe_unicode(names[i][D_NAME])
	    else
		names[i] = "_  " & maybe_unicode(names[i][D_NAME])
	    end if
	end for
	names = sort(names) -- directories first
	index = 3 * (find(">  ..", names) > 0)
	index = list_box("new file name:", names, index)
	if index = -1 then
	    ok = lfn_chdir("..") -- back to parent dir (BackSpace)
	elsif index > 0 then
	    if names[index][1] = '>' then -- new dir list
		ok = lfn_chdir(maybe_utf8(names[index][4..$]))
	    else -- open new file
		path = lfn_current_dir()
		if not help then
		    new_file_dir = path -- update local variable (utf8 string)
		end if
		if path[$] != SLASH then
		    path &= SLASH
		end if
		-- file_name is unicode-string
		file_name = maybe_unicode(path) & names[index][4..$]
		stop = TRUE
		exit
	    end if
	else -- (Esc) refresh full-screen window
	    DisplayWindow(b_line - s_line + 1, 1)
	    exit
	end if
    end while
    ok = lfn_chdir(save_dir) -- restore current directory
end procedure

procedure view_file(integer key)
-- key is F1 to F10. this code is moved from edit_file(), now used also by
-- get_escape(). Esc-v works like F1..F10; it's useful for Linux tty1, etc
    if key < F1[1] + length(window_list) then
	switch_window(key - F1[1] + 1)
	-- refresh full-screen window (Shian)
	DisplayWindow(b_line - s_line + 1, 1)
    else
	set_top_line("")
	putsu(sprintf("F%d is not an active window", key - F1[1] + 1))
	normal_video()
    end if
end procedure

procedure initialize_buffer()
-- (code moved to here by Shian)
    if length(buffer) = 0 then
	buffer = {{'\n'}} -- one line with \n
	b_line = 1
	b_col = 1
	s_line = 1
	s_col = 1
    end if
end procedure

procedure insert(char key)
-- insert a character into the current line at the current position

    string tail

    initialize_buffer()
    set_modified()

    tail = buffer[b_line][b_col..length(buffer[b_line])]
    if key = CR or key = '\n' then
	-- truncate this line and create a new line using tail
	buffer[b_line] = buffer[b_line][1..b_col-1] & '\n'

	-- make room for new line:
	buffer = append(buffer, 0)
	for i = length(buffer)-1 to b_line+1 by -1 do
	    buffer[i+1] = buffer[i]
	end for

	-- store new line
	buffer[b_line+1] = tail

	if s_line = window_length then
	    arrow_down()
	    arrow_up()
	elsif utf8_mode then
	    DisplayWindow(b_line - s_line + 1, 1)
	else -- ASCII mode
	    ScrollDown(s_line+1, window_length)
	end if
	if window_length = 1 then
	    arrow_down()
	elsif utf8_mode then
	    b_line += 1
	    s_line += 1
	else -- ASCII mode
	    DisplayLine(b_line, s_line, FALSE)
	    b_line += 1
	    s_line += 1
	    DisplayLine(b_line, s_line, FALSE)
	end if
	s_col = 1
	b_col = 1
    else
	buffer[b_line] = buffer[b_line][1..b_col-1] & key & tail
	DisplayLine(b_line, s_line, TRUE)
	if not freeze_cursor then
	    if key = TAB then
		s_col = tab(edit_tab_width, s_col)
	    else
		s_col += 1 + cjk_char(key) -->----- s_col += 1
	    end if
	    b_col += 1
	end if
    end if
    set_position(s_line, s_col)
end procedure

procedure insert_string(string text)
-- insert a bunch of characters at the current position
    natural save_line, save_col

    initialize_buffer()

    save_line = b_line
    save_col = b_col
    for i = 1 to length(text) do
	if text[i] = CR or text[i] = '\n' then
	    insert(text[i])
	else
	    buffer[b_line] = buffer[b_line][1..b_col-1] & text[i] &
			     buffer[b_line][b_col..length(buffer[b_line])]
	    b_col += 1
	    if i = length(text) then
		DisplayLine(b_line, s_line, FALSE)
	    end if
	end if
    end for
    goto_line(save_line, save_col)
end procedure

function numeric_base(string s, integer base)
-- does s without prefix contain only numeric characters? (Shian)
-- base is 2,8,10,16,etc
    if length(s) = 0 then
	return FALSE
    elsif base <= 10 then
	return not find(FALSE, (s >= '0' and s <= '0' - 1 + base))
    else -- base > 10
	return not find(FALSE,
	    (s >= '0' and s <= '9') or
	    (s >= 'A' and s <= 'A' - 11 + base) or
	    (s >= 'a' and s <= 'a' - 11 + base))
    end if
end function

procedure toggle_32bit_base()
-- toggle number at current cursor location between dec/hex/bin/oct (Shian)
-- number must be a 32-bit number. decimal and #hex can have minus sign.
-- only decimal can be converted to signed number, others are unsigned.
    sequence st_en, v
    integer st, en
    atom num
    string number, msg
    boolean is_hex, is_hex2, is_bin, is_oct, is_dec

    msg = "please locate the cursor on a number (|#|0x|0b|0o|...)"

    if length(buffer) = 0 then
	set_top_line(msg)
	return -- don't crash
    end if

    is_hex = FALSE
    is_hex2 = FALSE
    is_bin = FALSE
    is_oct = FALSE
    is_dec = FALSE

    st_en = get_current_word(TRUE) -- {start, end} of string
    if length(st_en) = 0 then
	set_top_line(msg)
	return -- current location is not a string
    end if
    st = st_en[1]
    en = st_en[2]
    number = buffer[b_line][st..en]

    -- what base is the current number?
    if st > 1 and buffer[b_line][st-1] = '#' and numeric_base(number, 16) then
	number = '#' & upper(number)
	st -= 1
	is_hex = TRUE
    elsif match("0x", number) = 1 and numeric_base(number[3..$], 16) then
	number = "0x" & upper(number[3..$])
	is_hex2 = TRUE
    elsif match("0b", number) = 1  and numeric_base(number[3..$], 2) then
	is_bin = TRUE
    elsif match("0o", number) = 1  and numeric_base(number[3..$], 8) then
	is_oct = TRUE
    elsif numeric_base(number, 10) then -- decimal should be last (just in case)
	is_dec = TRUE
    else
	set_top_line(msg) -- not a valid number
	return
    end if

    -- minus sign exists? (only for decimal and #hex)
    if st > 1 and buffer[b_line][st-1] = '-' and (is_hex or is_dec) then
	number = '-' & number
	st -= 1
    end if

    -- convert string to number
    if is_hex or is_dec then
	v = value(number) -- may have minus sign
	num = v[2]
    else
	num = int(number) -- int() return unsigned value
    end if
    if not dwords(num) then
	set_top_line("not a 32-bit number")
	return
    end if

    -- convert 32-bit number to string
    if is_hex or is_hex2 then
	number = bin(num)
    elsif is_bin then
	number = oct(num)
    elsif is_oct then
	set_top_line("signed or unsigned decimal? (s,u): ")
	if find('s', key_gets("su", EMPTY)) then
	    number = sprintf("%d", signed(num, 32))
	else -- unsigned
	    number = dec(num)
	end if
    elsif is_dec then
	number = hex(num)
    end if

    -- apply to buffer
    buffer[b_line] = buffer[b_line][1..st-1] & number &
	buffer[b_line][en+1..$]
    set_modified()
    normal_video()
    -- keep cursor at the start of number
    b_col = st + (number[1] = '-' or number[1] = '#')
    goto_line(b_line, b_col)
    DisplayLine(b_line, s_line, FALSE)
end procedure

procedure get_escape(boolean help)
-- process escape command
    sequence command, answer
    natural line
    object self_command

    cursor(ED_CURSOR)
    searching = FALSE -- bug fix, always reset searching on Esc (Shian)

    set_top_line("")
    if help then
	command = "h"
    else
	first_bold("help ")
	first_bold("clone ")
	first_bold("quit ")
	first_bold("save ")
	first_bold("write ")
	first_bold("new ")
	if file_type = FT_EUPHORIA then
	    if OS_LINUX then
		first_bold("exu ")
	    else
		first_bold("ex ")
	    end if
	elsif file_type = FT_C or file_type = FT_QBASIC then
	    first_bold("exe ")
	end if
	first_bold("dos ")
	first_bold("find/")
	first_bold("rep ")
	first_bold("tools ")
	first_bold("mods ")
	first_bold("view ")
	first_bold("jump ")
	text_color(TOP_LINE_TEXT_COLOR)
	putsu("ddd: ")
	command = key_gets("hcqswnedfrtmvj", EMPTY) & SPACE
    end if

    if command[1] = 'f' then
	replacing = FALSE
	searching = search(FALSE)

    elsif command[1] = 'r' then
	replacing = TRUE
	searching = search(FALSE)

    elsif command[1] = 'q' then
	if modified and last_use() then
	    set_top_line("quit without saving changes? (y,n): ")
	    if find('y', key_gets("yn", EMPTY)) then
		stop = delete_window()
	    end if
	else
	    stop = delete_window()
	end if

    elsif command[1] = 'c' then
	clone_window()

    elsif command[1] = 'n' then
	if modified and last_use() then
	    while TRUE do
		set_top_line("")
		putsu("save changes to " & file_name & "? (y,n): ")
		answer = key_gets("yn", EMPTY)
		if find('y', answer) then
		    save_file(file_name)
		    exit
		elsif find('n', answer) then
		    exit
		end if
	    end while
	end if
	save_state()
	set_top_line("new file name (Esc=List): ")
	answer = delete_trailing_white(key_gets("", file_history))
	if length(answer) != 0 then
	    file_name = answer
	    stop = TRUE
	else
	    new_file_from_list() -- (Shian)
	end if

    elsif command[1] = 'w' then
	save_file(file_name)
	stop = FALSE

    elsif command[1] = 's' then
	save_file(file_name)
	if stop then
	    stop = delete_window()
	end if

    elsif command[1] = 'e' then
	if modified then
	    save_file(file_name)
	    stop = FALSE
	end if
	-- execute the current file & return
	if file_type = FT_EUPHORIA then
	    if sequence(lfn_dir("ex.err")) then
		if delete_file("ex.err") then
		    clear_screen()
		end if
--              if OS_LINUX then
--                  system(delete_cmd & "ex.err", 0)
--              else
--                  system(delete_cmd & "ex.err > NUL", 0)
--              end if
	    end if
	    if OS_LINUX then
		shell("exu \"" & file_name & "\"")
	    elsif OS_WIN32 or -- Windows-10 can't run 16-bit ex.exe!
		match(".exw", lower(file_name)) or
		match(".ew",  lower(file_name)) then
		shell("exwc " & file_name)
	    else
		shell("ex " & lfn_short_path(file_name, 0))
	    end if
	    goto_line(0, b_col)
	    if equal(lfn_short_path(file_name, 0), get_err_line()) then
		goto_line(start_line, 1)
		set_err_pointer()
		show_message()
	    end if

	elsif file_type = FT_C then
	    if OS_LINUX then
		shell("eduexec \"" & file_name & "\"")
	    elsif OS_WIN32 then
		shell("eduexec.bat \"" & file_name & "\"")
	    elsif OS_DOS32 then
		shell("eduexec.bat \"" & lfn_short_path(file_name, 0) & "\"")
	    end if
	    goto_line(0, b_col)

	elsif file_type = FT_QBASIC then
	    if OS_LINUX then
		set_top_line("QBASIC is for DOS!")
		normal_video()
	    else
		shell("QBASIC " & lfn_short_path(file_name, 0))
		goto_line(0, b_col)
	    end if
	end if

    elsif command[1] = 'd' then
	if OS_LINUX then
	    set_top_line("Linux command? ")
	else
	    set_top_line("DOS command? ")
	end if
	command = key_gets("", command_history)
	if length(delete_trailing_white(command)) > 0 then
	    shell(command)
	    command_history = update_history(command_history, command)
	end if
	normal_video()
	goto_line(0, b_col) -- refresh screen

    elsif command[1] = 'm' then
	-- show differences between buffer and file on disk
	save_file(TEMPFILE)
	if stop then
	    stop = FALSE
	    shell(compare_cmd & lfn_short_path(file_name, 0) & SPACE & 
		TEMPFILE & " | more")
	    normal_video()
	    goto_line(0, b_col)
	    editbuff = TRUE
	end if

    elsif command[1] = 'h' then
	if atom(EUDIR) then
	    -- Euphoria hasn't been installed yet
	    set_top_line(EUDIR_NOT_SET)
	else
	    self_command = clone_new_self_command() & EUDIR & SLASH & "doc"
	    if help then
		set_top_line(
		"That key does nothing - do you want to view the help text? (y,n): ")
		answer = key_gets("yn", EMPTY) & SPACE
		if find('y', answer) then
		    answer = "e"
		else
		    answer = {SPACE} -- escape
		end if
	    else
		set_top_line(
		"edu, refman, library, database, lib2? (e,r,l,d,2): ")
		answer = key_gets("erld2", EMPTY) & SPACE
	    end if
	    if answer[1] = 'r' then
		add_queue(self_command & SLASH & "refman.doc" & CR)
	    elsif answer[1] = 'e' then
		add_queue(self_command & SLASH & "edu.doc" & CR)
	    elsif answer[1] = 'l' then
		add_queue(self_command & SLASH & "library.doc" & CR)
	    elsif answer[1] = 'd' then
		add_queue(self_command & SLASH & "database.doc" & CR)
	    elsif answer[1] = '2' then
		set_top_line(
		"machine2, string, math, date, random, utf8, file2, japi?" &
		" (m,s,a,d,r,u,f,j): ")
		answer = key_gets("msadrufj", EMPTY) & SPACE
		if answer[1] = 'm' then
		    add_queue(self_command & SLASH & "machine2.doc" & CR)
		elsif answer[1] = 's' then
		    add_queue(self_command & SLASH & "string.doc" & CR)
		elsif answer[1] = 'a' then
		    add_queue(self_command & SLASH & "math.doc" & CR)
		elsif answer[1] = 'd' then
		    add_queue(self_command & SLASH & "datetime.doc" & CR)
		elsif answer[1] = 'r' then
		    add_queue(self_command & SLASH & "random.doc" & CR)
		elsif answer[1] = 'u' then
		    add_queue(self_command & SLASH & "utf8.doc" & CR)
		elsif answer[1] = 'f' then
		    add_queue(self_command & SLASH & "file2.doc" & CR)
		elsif answer[1] = 'j' then
		    add_queue(self_command & SLASH & "japi.doc" & CR)
		else
		    normal_video()
		end if
	    else
		normal_video()
	    end if
	end if

    elsif command[1] = 't' then -- tools (Shian)
	set_top_line("case, time, number, search, directory, lines?" &
			    " (c, t, n, s, d, l): ")
	answer = key_gets("ctnsdl", EMPTY) & SPACE -- SPACE avoid error on ""

	if answer[1] = 'c' then -- change case (a-z,A-Z)
	    set_top_line("lower, upper current word? (l, u): ")
	    answer = key_gets("lu", EMPTY) & SPACE
	    if answer[1] = 'l' then
		answer = lower(get_current_word(FALSE))
	    elsif answer[1] = 'u' then
		answer = upper(get_current_word(FALSE))
	    else
		answer = ""
	    end if
	    if length(answer) then
		-- note: add_queue() will not work here with macro recording.
		command = get_current_word(TRUE) -- {start, end}
		buffer[b_line] = buffer[b_line][1..command[1]-1] & answer &
		    buffer[b_line][command[2]+1..$]
		set_modified()
		normal_video()
		DisplayLine(b_line, s_line, FALSE)
	    end if

	elsif answer[1] = 's' then -- search in dir using 'search.ex'
	    set_top_line("search in directory: ")
	    answer = delete_trailing_white(key_gets("", search_dir_history))
	    if length(answer) then
		search_dir_history = update_history(search_dir_history, answer)
		command = lfn_current_dir() -- save current dir
		if lfn_chdir(maybe_utf8(answer)) then
		    if OS_WIN32 then -- Windows-10 don't support ex.exe
			shell(EUDIR & SLASH & "bin" & SLASH & "exwc search.ex")
		    else
			shell(EUDIR & SLASH & "bin" & SLASH & "search")
		    end if
		    if lfn_chdir(command) then end if -- restore current dir
		    -- normal_video() -- DisplayWindow(b_line - s_line + 1, 1)
		    if OS_LINUX then
			self_command = getenv("HOME")
			if atom(self_command) then
			    self_command = '.'
			end if
		    else
			self_command = "C:"
		    end if
		    add_queue(clone_new_self_command() & self_command &
			SLASH & "search.out" & CR)
		else
		    set_top_line("can't search directory '" & answer & '\'')
		end if
	    end if

	elsif answer[1] = 'd' then -- change current dir 
	    -- note: on Linux you cannot change dir using Esc-d (dos)
	    set_top_line("change directory: ")
	    answer = delete_trailing_white(key_gets("", change_dir_history))
	    if length(answer) then
		change_dir_history = update_history(change_dir_history, answer)
		if lfn_chdir(maybe_utf8(answer)) then
		    if not OS_LINUX then -- change also drive on Windows/DOS
			answer = full_dirname(maybe_utf8(answer)) & EMPTY
			if chdrive(answer[1]) then
			end if
		    end if
		end if
	    end if
	    set_top_line("current directory is: " & 
		maybe_unicode(lfn_current_dir()))

	elsif answer[1] = 't' then  -- insert date-time at current location
	    -- note: add_queue() will not work here with macro recording.
	    set_top_line("short or long time? (s, l): ")
	    answer = key_gets("sl", EMPTY) & SPACE
	    normal_video()
	    if answer[1] = 's' then
		insert_string(sprintd(SHORT_DATE_FORMAT, now()))
		set_modified()
	    elsif answer[1] = 'l' then
		insert_string(sprintd(LONG_DATE_FORMAT, now()))
		set_modified()
	    end if

	elsif answer[1] = 'n' then -- toggle number dec/hex/bin/oct (un|signed)
	    toggle_32bit_base()

	elsif answer[1] = 'l' then
	    new_screen_length()
	end if

	normal_video()

    elsif command[1] >= '0' and command[1] <= '9' then
	line = numeric(command)
	normal_video()
	goto_line(line, 1)
	if not buffer_line(line) then
	    set_top_line("")
	    putsu(sprintf("lines are 1..%d", length(buffer)))
	end if

    elsif command[1] = 'v' then -- view (Shian)
	answer = "view file (Esc=List, "
	for num = 1 to length(window_list) do
	    answer &= (num * (num < 10) + '0') & ", "
	end for
	set_top_line(answer[1..$ - 2] & "): ")
	answer = key_gets("1234567890", EMPTY) & SPACE
	normal_video()
	if answer[1] >= '1' and answer[1] <= '9' then
	    view_file(F1[1] + answer[1] - '1')
	elsif answer[1] = '0' then
	    view_file(F10[1])
	else -- display list box
	    answer = EMPTY -- list of file names
	    for i = 1 to length(window_list) do
		answer = append(answer, sprintf("(F%d) ",i) & window_list[i][10])
	    end for
	    line = list_box("view file:", answer, window_number)
	    if line > 0 then
		switch_window(line)
	    end if
	    DisplayWindow(b_line - s_line + 1, 1) -- refresh full-screen window
	end if

    elsif command[1] = 'j' then -- jump (Shian)
	jump()

    else
	set_top_line("")
	if length(buffer) = 0 then
	    putsu("empty buffer")
	else
	    putsu(file_name & sprintf(" line %d of %d, column %d of %d, ",
		       {b_line, length(buffer), s_col,
			length(expand_tabs(edit_tab_width, buffer[b_line]))-1}))
	    if modified then
		putsu("modified")
	    else
		putsu("not modified")
	    end if
	end if
    end if

    normal_video()
end procedure

-- expandable words & corresponding text
constant expand_word = {"if", "for", "while", "elsif",
			"procedure", "type", "function"},

	 expand_text = {" then",  "=  to  by  do",  " do",  " then",
			"()",  "()",  "()"
		       }

procedure try_auto_complete(char key)
-- check for a keyword that can be automatically completed
    string word, this_line, leading_white, white_space, begin
    natural first_non_blank, wordnum

    if key = SPACE then
	insert(key)
    end if
    this_line = buffer[b_line]
    white_space = (this_line = SPACE or this_line = TAB) -- (string type)
    first_non_blank = find(0, white_space) -- there's always '\n' at end
    leading_white = this_line[1..first_non_blank - 1]
    if auto_complete and first_non_blank < b_col - 2 then
	if not find(0, white_space[b_col..$-1]) then
	    word = this_line[first_non_blank..b_col - 1 - (key = SPACE)]
	    wordnum = find(word, expand_word)

	    if key = CR and equal(word, "else") then
		 leading_white &= TAB

	    elsif wordnum > 0 then
		-- expandable word (only word on line)

		begin = expand_text[wordnum] & CR & leading_white

		if equal(word, "elsif") then
		    insert_string(begin & TAB)

		elsif find(word, {"function", "type"}) then
		    insert_string(begin & CR &
				  leading_white & "\treturn" & CR &
				  "end " & expand_word[wordnum])
		else
		    insert_string(begin & TAB & CR &
				  leading_white &
				  "end " & expand_word[wordnum])
		end if
		short_beep()
	    end if
	end if
    end if
    if key = CR then
	if b_col >= first_non_blank then
	    buffer[b_line] = buffer[b_line][1..b_col-1] & leading_white &
			     buffer[b_line][b_col..length(buffer[b_line])]
	    insert(CR)
	    skip_white()
	else
	    insert(CR)
	end if
    end if
end procedure

procedure insert_kill_buffer()
-- insert the kill buffer at the current position
-- kill buffer could be a sequence of lines or a sequence of characters

    if length(kill_buffer) = 0 then
	return
    end if
    set_modified()
    if atom(kill_buffer[1]) then
	-- inserting a sequence of chars
	insert_string(kill_buffer)
    else
	-- inserting a sequence of lines
	buffer = buffer[1..b_line - 1] & kill_buffer & buffer[b_line..$]
	DisplayWindow(b_line, s_line)
	b_col = 1
	s_col = 1
    end if
end procedure

procedure delete_line(buffer_line dead_line)
-- delete a line from the buffer and update the display if necessary

    integer x

    set_modified()
    -- move up all lines coming after the dead line
    for i = dead_line to length(buffer)-1 do
	buffer[i] = buffer[i+1]
    end for
    buffer = buffer[1..$-1]

    x = dead_line - b_line + s_line
    if window_line(x) then
	-- dead line is on the screen at line x
	if utf8_mode then
	    DisplayWindow(b_line - s_line + 1, 1)
	else -- ASCII mode
	    ScrollUp(x, window_length)
	    if length(buffer) - b_line >= window_length - s_line then
		-- show new line at bottom
		DisplayLine(b_line + window_length - s_line, window_length, TRUE)
	    end if
	end if
    end if
    if b_line > length(buffer) then
	arrow_up()
    else
	b_col = 1
	s_col = 1
    end if
    adding_to_kill = TRUE
end procedure

procedure delete_char()
-- delete the character at the current position
    char dchar
    string head
    natural save_b_col

    set_modified()
    dchar = buffer[b_line][b_col]
    head = buffer[b_line][1..b_col - 1]
    if dchar = '\n' then
	if b_line < length(buffer) then
	    -- join this line with the next one and delete the next one
	    buffer[b_line] = head & buffer[b_line+1]
	    DisplayLine(b_line, s_line, FALSE)
	    save_b_col = b_col
	    delete_line(b_line + 1)
	    for i = 1 to save_b_col - 1 do
		arrow_right()
	    end for
	else
	    if length(buffer[b_line]) = 1 then
		delete_line(b_line)
	    else
		arrow_left() -- a line must always end with \n
	    end if
	end if
    else
	buffer[b_line] = head & buffer[b_line][b_col+1..length(buffer[b_line])]
	if length(buffer[b_line]) = 0 then
	    delete_line(b_line)
	else
	    DisplayLine(b_line, s_line, FALSE)
	    if b_col > length(buffer[b_line]) then
		arrow_left()
	    end if
	end if
    end if
    adding_to_kill = TRUE
end procedure

function good(extended_char key)
-- return TRUE if key should be processed
    if find(key, SPECIAL_KEYS) then
	return TRUE
    elsif key >= SPACE and key <= 255 then --(some PCs return >255 on DOS!)
	return TRUE
    elsif key = TAB or key = CR then
	return TRUE
    elsif utf8_mode and key >= SPACE and key <= MAX_UNICODE then
	return TRUE
    else
	return FALSE
    end if
end function

procedure edit_file()
-- edit the file in buffer
    extended_char key

    if length(buffer) > 0 then
	if start_line > 0 then
	    if start_line > length(buffer) then
		start_line = length(buffer)
	    end if
	    goto_line(start_line, 1)
	    set_err_pointer()
	    show_message()
	end if
    end if

    -- to speed up keyboard repeat rate:
    -- system("mode con rate=32 delay=1", 2)

    cursor(ED_CURSOR)
    stop = FALSE

    if goto_help_line > 0 then -- context sensitive help
	goto_line(goto_help_line, 1)
	goto_help_line = 0
    end if

    while not stop do

	key = next_key()
	if key = CONTROL_C then
	    refresh_all_windows()
	    goto_line(0, b_col)
	end if

	if good(key) then
	    -- normal key

	    if key = CUSTOM_KEY then
		add_queue(CUSTOM_KEYSTROKES[file_type])

	    elsif key >= F1[1] and key <= F10[1] then
		view_file(key) -- code moved (Shian)
		adding_to_kill = FALSE

	    elsif length(buffer) = 0 and key != ESCAPE and
		key != MACRO_REC_KEY
	    then
		-- empty buffer
		-- only allowed action is to insert something
		if key = INSERT[1] or not find(key, SPECIAL_KEYS) then
		    initialize_buffer() -- code moved (Shian)
		    if key = INSERT[1] then
			insert_kill_buffer()
		    else
			insert(key)
		    end if
		    DisplayLine(1, 1, FALSE)
		end if

	    elsif key = DELETE[1] or key = XDELETE[1] then
		if not adding_to_kill then
		    kill_buffer = {buffer[b_line][b_col]}
		elsif string(kill_buffer[1]) then
		    -- we were building up deleted lines,
		    -- but now we'll switch to chars
		    kill_buffer = {buffer[b_line][b_col]}
		else
		    kill_buffer = append(kill_buffer, buffer[b_line][b_col])
		end if
		delete_char()

	    elsif key = CONTROL_DELETE or key = CONTROL_D then
		if not adding_to_kill then
		    kill_buffer = {buffer[b_line]}
		elsif atom(kill_buffer[1]) then
		    -- we were building up deleted chars,
		    -- but now we'll switch to lines
		    kill_buffer = {buffer[b_line]}
		else
		    kill_buffer = append(kill_buffer, buffer[b_line])
		end if
		delete_line(b_line)

	    else
		if key = PAGE_DOWN[1] or key = CONTROL_P then
		    page_down()

		elsif key = PAGE_UP[1] or key = CONTROL_U then
		    page_up()

		elsif key = ARROW_LEFT[1] then
		    arrow_left()

		elsif key = ARROW_RIGHT[1] then
		    arrow_right()

		elsif key = CONTROL_ARROW_LEFT[1] or key = CONTROL_L then
		    previous_word()

		elsif key = CONTROL_ARROW_RIGHT[1] or key = CONTROL_R then
		    next_word()

		elsif key = ARROW_DOWN[1] then
		    arrow_down()

		elsif key = ARROW_UP[1] then
		    arrow_up()

		elsif key = SPACE then
		    try_auto_complete(key)

		elsif key = INSERT[1] then
		    insert_kill_buffer()

		elsif key = BS then
		    if b_col > 1 then -- (bug fix at column 1, Shian)
			arrow_left()
			delete_char()
		    end if

		elsif key = HOME[1] then
		    b_col = 1
		    s_col = 1

		elsif key = END[1] then
		    goto_line(b_line, length(buffer[b_line]))

		elsif key = CONTROL_HOME[1] or key = CONTROL_T then
		    goto_line(1, 1)

		elsif key = CONTROL_END[1] or key = CONTROL_B then
		    goto_line(length(buffer), length(buffer[$]))

		elsif key = MACRO_REC_KEY then -- (Shian)
		    macro_recording = not macro_recording
		    if macro_recording then -- start recording
			macro_buffer = EMPTY
			macro_execute = 0
		    end if

		elsif key = MACRO_EXEC_KEY then -- (Shian)
		    if not macro_recording then
			macro_execute = length(macro_buffer)
		    end if

		elsif key = ESCAPE then
		    -- special command
		    get_escape(FALSE)

		elsif key = CR then
		    if searching then
			searching = search(TRUE)
			normal_video()
			searching = TRUE -- avoids accidental <CR> insertion
		    else
			try_auto_complete(key)
		    end if

		elsif key = CONTROL_F then
		    freeze_cursor = not freeze_cursor

		elsif key = CONTROL_K then
		    context_sensitive_help()

		elsif key = CAPS_LOCK then
		    -- ignore

		else
		    insert(key)

		end if

		adding_to_kill = FALSE

	    end if

	    if key != CR and key != ESCAPE then
		searching = FALSE
	    end if
	    cursor(ED_CURSOR)

	elsif key = 506 and not utf8_mode then
	    -- ignore special cases
	    -- CapsLock returns 506 on DOS32 on some PCs...
	else
	    -- illegal key pressed
	    get_escape(TRUE)  -- give him some help
	end if
	set_position(s_line, s_col)
	set_status_line(EMPTY) -- update editing status (Shian)
    end while
end procedure

procedure get_linux_start_position()
-- patch to fix cursor position on start when asking for "file name:" (Shian)
-- credit for idea: Ken Rhodes, http://openeuphoria.org/forum/131848.wc
    sequence ret
    integer key, row, col

    if not OS_LINUX then
	return -- this patch works for Linux ANSI/VT100 terminal type
    end if
    ret = EMPTY
    puts(1, 27 & "[6n")     -- query cursor position, <ESC>[6n
    pause() -- wait for terminal to response (or user press key)

    -- get reported cursor position, <ESC>[{ROW};{COLUMN}R
    for i = 1 to 15 do
	key = get_key()
	   if key = '[' then ret &= '{'
	elsif key = ';' then ret &= ','
	elsif key >= '0' and key <= '9' then
	    ret &= key
	elsif key = 'R' then ret &= '}'
	    exit
	end if
    end for
    ret = value(ret) -- (ret) can be "{2,10}"

    -- make sure edu don't crash if reported value is invalid
    if ret[1] = GET_SUCCESS and length(ret[2]) = 2 then
	row = floor(ret[2][1])
	col = floor(ret[2][2])
	if row >= 1 and row <= screen_length and
	    col >= 1 and col <= screen_width
	then
	    position(row, col) -- update cursor position for Euphoria
	end if
    end if
end procedure

procedure ed(sequence command)
-- editor main procedure
-- start editing a new file
-- edu.ex is executed by edu.bat
-- command line will be:
--    ex edu.ex              - get filename from ex.err, or user
--    ex edu.ex filename     - filename
--    ex edu.ex -u filename  - filename -- specified as utf8-string

    file_number file_no
    object fullpath
    
    start_line = 0
    start_col = 0

    if length(command) >= 3 then
	file_name = maybe_unicode(command[3])
	if not OS_LINUX then
	    file_name = lower(file_name)
	end if
    else
	file_name = get_err_line()
    end if
    if not utf8_mode then
	wrap(0) -- putsu() replaces wrap() for supporting UTF-8 mode
    end if
    if length(file_name) = 0 then
	-- we still don't know the file name - so ask user
	get_linux_start_position() -- a patch for Linux (Shian)
	putsu("file name: ")
	cursor(ED_CURSOR)
	file_name = key_gets("", file_history) -- file_name is unicode-string
	putsu("\n")
    end if
    file_name = delete_trailing_white(file_name)
    if length(file_name) = 0 then
	abort(1) -- file_name was just whitespace - quit
    end if
    -- for showing the full (LFN) file path in title bar
    fullpath = full_pathname(maybe_utf8(file_name))
    if sequence(fullpath) then -- otherwise, directory not exist (ignore)
	file_name = maybe_unicode(fullpath)
    end if  
    file_history = update_history(file_history, file_name)
    file_no = lfn_open(maybe_utf8(file_name), "r")

    -- turn off multi_color & auto_complete for non .e files
    multi_color = WANT_COLOR_SYNTAX
    auto_complete = WANT_AUTO_COMPLETE
    if not config[VC_COLOR] or config[VC_MODE] = 7 then
	multi_color = FALSE -- mono monitor
    end if
    file_name &= SPACE
    file_type = FT_NONE
    -- which file type is it, Euphoria/C/Qbasic?
    for i = 1 to length(E_FILES) do
	if match(lower(E_FILES[i]) & SPACE, lower(file_name)) then
	    file_type = FT_EUPHORIA
	    exit
	end if
    end for
    for i = 1 to length(C_FILES) * (file_type = FT_NONE) do
	if match(lower(C_FILES[i]) & SPACE, lower(file_name)) then
	    file_type = FT_C
	    exit
	end if
    end for
    for i = 1 to length(B_FILES) * (file_type = FT_NONE) do
	if match(lower(B_FILES[i]) & SPACE, lower(file_name)) then
	    file_type = FT_QBASIC
	    exit
	end if
    end for
    if file_type = FT_NONE then
	multi_color = FALSE
    end if
    if file_type != FT_EUPHORIA then
	auto_complete = FALSE
    end if

    -- use PROG_INDENT tab width for Euphoria & other languages:
    edit_tab_width = STANDARD_TAB_WIDTH
    for i = 1 to length(PROG_FILES) do
       if match(PROG_FILES[i] & SPACE, file_name) then
	   edit_tab_width = PROG_INDENT
	   exit
       end if
    end for

    if multi_color then
	init_class()
    end if

    file_name = file_name[1..$-1] -- remove SPACE
    adding_to_kill = FALSE
    clear_modified()
    buffer_version = 0
    control_chars = FALSE
    cr_removed = FALSE
    new_buffer()
    s_line = 1
    s_col = 1
    b_line = 1
    b_col = 1
    save_state()
    clear_keyboard() -- to reduce "key bounce" problems
    if file_no = -1 then
	set_f_line(window_number, " <new file>")
	normal_video()
	ClearWindow()
    else
	set_f_line(window_number, "")
	normal_video()
	set_position(1, 1)
	cursor(NO_CURSOR)
	read_file(file_no)
	close(file_no)
    end if
    set_position(1, 1)
    set_status_line(EMPTY) -- (Shian)
    edit_file()
end procedure

procedure ed_main()
-- startup and shutdown of ed()
    sequence cl

    allow_break(FALSE)

    cl = command_line()

    if length(cl) >= 3 and equal(cl[3], "-u") then
	utf8_mode = TRUE    -- edit in UTF-8 mode (ex edu -u filename.e)
	cl = cl[1..2] & cl[4..$]    -- we don't need "-u" anymore
    else
	utf8_mode = FALSE   -- edit in ASCII mode (ex edu filename.e)
    end if

    config = video_config()

    if config[VC_XPIXELS] > 0 then
	if graphics_mode(3) then
	end if
	config = video_config()
    end if

    if config[VC_LINES] != INITIAL_LINES then
	screen_length = text_rows(INITIAL_LINES)
	config = video_config()
    end if
    screen_length = config[VC_LINES]
    screen_width = config[VC_COLUMNS] - utf8_mode -- less 1 column in utf8_mode
    BLANK_LINE = repeat(SPACE, screen_width)
    window_length = screen_length - 2 -- exclude top & status lines (Shian)

    while length(window_list) > 0 do
	ed(cl)
	cl = {"ex", "edu.ex" , maybe_utf8(file_name)}
    end while

    -- exit editor
    delete_editbuff()
    if screen_length != FINAL_LINES then
	screen_length = text_rows(FINAL_LINES)
    end if
    cursor(UNDERLINE_CURSOR)
    bk_color(BLACK)
    text_color(WHITE)
    position(screen_length, 1)
    putsu(BLANK_LINE)
    position(screen_length, 1)
    putsu("\n")
end procedure

ed_main()
-- This abort statement reduces the chance of
-- a syntax error when you edit edu.ex using itself:
if OS_LINUX then
    free_console()
end if
abort(0)
