1649 lines
42 KiB
Bash
Executable File
1649 lines
42 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -uo pipefail
|
|
|
|
# Released under the terms of the WTFPL license https://www.wtfpl.net/
|
|
|
|
# Bim was partially created with bim
|
|
# Does that make it a recursive text editor?
|
|
|
|
# Behold! Vim in bash. Has anyone else ever created something so diabolically evil?
|
|
|
|
filePath="${1:-}"
|
|
mode="command"
|
|
statusMessage=""
|
|
renderedMessage=$'\001'
|
|
pendingInput=""
|
|
pendingCommand=""
|
|
commandCount=""
|
|
bimCommandActive=0
|
|
bimCommandName=""
|
|
lastChangeKeys=""
|
|
insertChangeKeys=""
|
|
replayKeysRemaining=0
|
|
hasYank=0
|
|
yankType="line"
|
|
dirty=0
|
|
cursorRow=0
|
|
cursorCol=0
|
|
topRow=0
|
|
declare -a lines=("")
|
|
declare -a yankBuffer=()
|
|
declare -a undoLines=()
|
|
hasUndo=0
|
|
undoCursorRow=0
|
|
undoCursorCol=0
|
|
undoTopRow=0
|
|
undoDirty=0
|
|
insertUndoSaved=0
|
|
startupMessage=""
|
|
declare -A bimCommands=()
|
|
|
|
trim_whitespace() {
|
|
local value="$1"
|
|
value="${value#"${value%%[![:space:]]*}"}"
|
|
value="${value%"${value##*[![:space:]]}"}"
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
load_bimrc() {
|
|
local bimrcPath="${HOME:-}/.bimrc"
|
|
local permissions line trimmed name commandText invalidCount=0
|
|
|
|
[[ -n "${HOME:-}" && -e "$bimrcPath" ]] || return
|
|
|
|
if [[ -L "$bimrcPath" || ! -f "$bimrcPath" || ! -O "$bimrcPath" ]]; then
|
|
startupMessage="Ignoring unsafe ~/.bimrc"
|
|
return
|
|
fi
|
|
|
|
permissions="$(stat -c %A "$bimrcPath" 2>/dev/null)" || {
|
|
startupMessage="Could not inspect ~/.bimrc"
|
|
return
|
|
}
|
|
if [[ "${permissions:5:1}" == "w" || "${permissions:8:1}" == "w" ]]; then
|
|
startupMessage="Ignoring group/world-writable ~/.bimrc"
|
|
return
|
|
fi
|
|
|
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
|
trimmed="$(trim_whitespace "$line")"
|
|
[[ -z "$trimmed" || "${trimmed:0:1}" == "#" ]] && continue
|
|
if [[ "$line" != *"="* ]]; then
|
|
((invalidCount++))
|
|
continue
|
|
fi
|
|
name="$(trim_whitespace "${line%%=*}")"
|
|
commandText="$(trim_whitespace "${line#*=}")"
|
|
if [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_-]*$ && -n "$commandText" ]]; then
|
|
bimCommands["$name"]="$commandText"
|
|
else
|
|
((invalidCount++))
|
|
fi
|
|
done < "$bimrcPath"
|
|
|
|
if ((invalidCount > 0)); then
|
|
startupMessage="Ignored $invalidCount invalid ~/.bimrc line(s)"
|
|
fi
|
|
}
|
|
|
|
load_bimrc
|
|
|
|
if [[ -n "$filePath" && -f "$filePath" ]]; then
|
|
mapfile -t lines < "$filePath"
|
|
((${#lines[@]} == 0)) && lines=("")
|
|
fi
|
|
|
|
cleanup() {
|
|
local rows
|
|
rows="$(tput lines 2>/dev/null || printf '24')"
|
|
printf '\033[H\033[J\033[%s;1H\033[?25h\033[?1049l' "$rows"
|
|
stty "$savedStty"
|
|
printf '\n'
|
|
}
|
|
|
|
die() {
|
|
cleanup
|
|
printf 'bim: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
savedStty="$(stty -g)"
|
|
trap cleanup EXIT
|
|
trap 'exit 130' INT TERM
|
|
stty raw -echo
|
|
printf '\033[?1049h\033[?25l'
|
|
statusMessage="$startupMessage"
|
|
|
|
screen_size() {
|
|
screenRows="$(tput lines 2>/dev/null || printf '24')"
|
|
screenCols="$(tput cols 2>/dev/null || printf '80')"
|
|
editRows=$((screenRows - 1))
|
|
((editRows < 1)) && editRows=1
|
|
}
|
|
|
|
line_length() {
|
|
local row="$1"
|
|
printf '%s' "${#lines[row]}"
|
|
}
|
|
|
|
clamp_cursor() {
|
|
local maxRow=$(( ${#lines[@]} - 1 ))
|
|
((cursorRow < 0)) && cursorRow=0
|
|
((cursorRow > maxRow)) && cursorRow="$maxRow"
|
|
|
|
local maxCol
|
|
maxCol="$(line_length "$cursorRow")"
|
|
((cursorCol < 0)) && cursorCol=0
|
|
((cursorCol > maxCol)) && cursorCol="$maxCol"
|
|
|
|
if ((cursorRow < topRow)); then
|
|
topRow="$cursorRow"
|
|
elif ((cursorRow >= topRow + editRows)); then
|
|
topRow=$((cursorRow - editRows + 1))
|
|
fi
|
|
}
|
|
|
|
file_info() {
|
|
local name="${filePath:-[No Name]}"
|
|
local dirtyMark=""
|
|
[[ "$dirty" == 1 ]] && dirtyMark=" [+]"
|
|
statusMessage="${name}${dirtyMark} line $((cursorRow + 1)) of ${#lines[@]}, col $((cursorCol + 1))"
|
|
}
|
|
|
|
draw_message() {
|
|
local message="$statusMessage"
|
|
if ((bimCommandActive)); then
|
|
message=",$bimCommandName"
|
|
elif [[ -n "$pendingCommand" || -n "$commandCount" ]]; then
|
|
message="${commandCount}${pendingCommand}"
|
|
fi
|
|
|
|
if [[ "$message" != "$renderedMessage" ]]; then
|
|
printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$message"
|
|
renderedMessage="$message"
|
|
fi
|
|
}
|
|
|
|
draw_screen() {
|
|
screen_size
|
|
clamp_cursor
|
|
|
|
printf '\033[H'
|
|
local row line text
|
|
for ((row = 0; row < editRows; row++)); do
|
|
line=$((topRow + row))
|
|
printf '\033[%s;1H\033[K' "$((row + 1))"
|
|
if ((line < ${#lines[@]})); then
|
|
text="${lines[line]}"
|
|
printf '%-*.*s' "$screenCols" "$screenCols" "$text"
|
|
else
|
|
printf '~'
|
|
fi
|
|
done
|
|
|
|
draw_message
|
|
if [[ "$mode" == "command" ]] && ((${#lines[cursorRow]} > 0 && cursorCol >= ${#lines[cursorRow]})); then
|
|
cursorCol=$(( ${#lines[cursorRow]} - 1 ))
|
|
fi
|
|
printf '\033[%s;%sH' "$((cursorRow - topRow + 1))" "$((cursorCol + 1))"
|
|
}
|
|
|
|
read_key() {
|
|
local char next third rest
|
|
if [[ -n "$pendingInput" ]]; then
|
|
key="${pendingInput:0:1}"
|
|
pendingInput="${pendingInput:1}"
|
|
((replayKeysRemaining > 0)) && ((replayKeysRemaining--))
|
|
return 0
|
|
fi
|
|
|
|
IFS= read -rsN1 char || return 1
|
|
if [[ "$char" == $'\033' ]]; then
|
|
if IFS= read -rsN1 -t 0.01 next; then
|
|
if [[ "$next" == "[" ]]; then
|
|
rest="$next"
|
|
while IFS= read -rsN1 -t 0.001 char; do
|
|
rest+="$char"
|
|
done
|
|
key=$'\033'"$rest"
|
|
elif [[ "$next" == "O" ]]; then
|
|
if IFS= read -rsN1 -t 0.001 third; then
|
|
if [[ "$third" =~ [ABCD] ]]; then
|
|
key=$'\033'"O$third"
|
|
else
|
|
pendingInput="O$third"
|
|
key=$'\033'
|
|
fi
|
|
else
|
|
pendingInput="O"
|
|
key=$'\033'
|
|
fi
|
|
else
|
|
pendingInput="$next"
|
|
key=$'\033'
|
|
fi
|
|
else
|
|
key=$'\033'
|
|
fi
|
|
else
|
|
key="$char"
|
|
fi
|
|
}
|
|
|
|
move_left() {
|
|
if ((cursorCol > 0)); then
|
|
((cursorCol--))
|
|
fi
|
|
}
|
|
|
|
move_right() {
|
|
local maxCol
|
|
maxCol="$(line_length "$cursorRow")"
|
|
if ((cursorCol < maxCol)); then
|
|
((cursorCol++))
|
|
fi
|
|
}
|
|
|
|
move_up() {
|
|
((cursorRow > 0)) && ((cursorRow--))
|
|
}
|
|
|
|
move_down() {
|
|
((cursorRow + 1 < ${#lines[@]})) && ((cursorRow++))
|
|
}
|
|
|
|
is_blank_char() {
|
|
local char="$1"
|
|
[[ "$char" =~ [[:space:]] ]]
|
|
}
|
|
|
|
is_last_buffer_position() {
|
|
local row="$1"
|
|
local col="$2"
|
|
((row == ${#lines[@]} - 1 && col >= ${#lines[row]}))
|
|
}
|
|
|
|
step_position_forward() {
|
|
local -n rowRef="$1"
|
|
local -n colRef="$2"
|
|
if ((colRef < ${#lines[rowRef]})); then
|
|
((colRef++))
|
|
elif ((rowRef + 1 < ${#lines[@]})); then
|
|
((rowRef++))
|
|
colRef=0
|
|
fi
|
|
}
|
|
|
|
step_position_backward() {
|
|
local -n rowRef="$1"
|
|
local -n colRef="$2"
|
|
if ((colRef > 0)); then
|
|
((colRef--))
|
|
elif ((rowRef > 0)); then
|
|
((rowRef--))
|
|
colRef="${#lines[rowRef]}"
|
|
fi
|
|
}
|
|
|
|
move_word_forward() {
|
|
local count="$1"
|
|
local i row col char
|
|
for ((i = 0; i < count; i++)); do
|
|
row="$cursorRow"
|
|
col="$cursorCol"
|
|
if is_last_buffer_position "$row" "$col"; then
|
|
break
|
|
fi
|
|
|
|
if ((col < ${#lines[row]})); then
|
|
char="${lines[row]:col:1}"
|
|
if ! is_blank_char "$char"; then
|
|
while ! is_last_buffer_position "$row" "$col"; do
|
|
if ((col >= ${#lines[row]})); then
|
|
break
|
|
fi
|
|
char="${lines[row]:col:1}"
|
|
is_blank_char "$char" && break
|
|
step_position_forward row col
|
|
done
|
|
fi
|
|
fi
|
|
|
|
while ! is_last_buffer_position "$row" "$col"; do
|
|
if ((col >= ${#lines[row]})); then
|
|
step_position_forward row col
|
|
continue
|
|
fi
|
|
char="${lines[row]:col:1}"
|
|
! is_blank_char "$char" && break
|
|
step_position_forward row col
|
|
done
|
|
|
|
cursorRow="$row"
|
|
cursorCol="$col"
|
|
done
|
|
}
|
|
|
|
move_word_backward() {
|
|
local count="$1"
|
|
local i row col char
|
|
for ((i = 0; i < count; i++)); do
|
|
row="$cursorRow"
|
|
col="$cursorCol"
|
|
if ((row == 0 && col == 0)); then
|
|
break
|
|
fi
|
|
|
|
step_position_backward row col
|
|
|
|
while ((row > 0 || col > 0)); do
|
|
if ((col >= ${#lines[row]})); then
|
|
step_position_backward row col
|
|
continue
|
|
fi
|
|
char="${lines[row]:col:1}"
|
|
! is_blank_char "$char" && break
|
|
step_position_backward row col
|
|
done
|
|
|
|
while ((row > 0 || col > 0)); do
|
|
if ((col == 0)); then
|
|
break
|
|
fi
|
|
char="${lines[row]:col-1:1}"
|
|
is_blank_char "$char" && break
|
|
step_position_backward row col
|
|
done
|
|
|
|
cursorRow="$row"
|
|
cursorCol="$col"
|
|
done
|
|
}
|
|
|
|
move_word_end_forward() {
|
|
local count="$1"
|
|
local i row col nextRow nextCol char nextChar
|
|
for ((i = 0; i < count; i++)); do
|
|
row="$cursorRow"
|
|
col="$cursorCol"
|
|
if is_last_buffer_position "$row" "$col"; then
|
|
break
|
|
fi
|
|
|
|
if ((col < ${#lines[row]})); then
|
|
char="${lines[row]:col:1}"
|
|
nextRow="$row"
|
|
nextCol="$col"
|
|
step_position_forward nextRow nextCol
|
|
if ! is_blank_char "$char"; then
|
|
if ((nextCol >= ${#lines[nextRow]})); then
|
|
row="$nextRow"
|
|
col="$nextCol"
|
|
else
|
|
nextChar="${lines[nextRow]:nextCol:1}"
|
|
if is_blank_char "$nextChar"; then
|
|
row="$nextRow"
|
|
col="$nextCol"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
while ! is_last_buffer_position "$row" "$col"; do
|
|
if ((col >= ${#lines[row]})); then
|
|
step_position_forward row col
|
|
continue
|
|
fi
|
|
char="${lines[row]:col:1}"
|
|
! is_blank_char "$char" && break
|
|
step_position_forward row col
|
|
done
|
|
|
|
while ! is_last_buffer_position "$row" "$col"; do
|
|
nextRow="$row"
|
|
nextCol="$col"
|
|
step_position_forward nextRow nextCol
|
|
if ((nextCol >= ${#lines[nextRow]})); then
|
|
break
|
|
fi
|
|
nextChar="${lines[nextRow]:nextCol:1}"
|
|
is_blank_char "$nextChar" && break
|
|
row="$nextRow"
|
|
col="$nextCol"
|
|
done
|
|
|
|
cursorRow="$row"
|
|
cursorCol="$col"
|
|
done
|
|
}
|
|
|
|
move_word_end_backward() {
|
|
local count="$1"
|
|
local i row col char
|
|
for ((i = 0; i < count; i++)); do
|
|
row="$cursorRow"
|
|
col="$cursorCol"
|
|
if ((row == 0 && col == 0)); then
|
|
break
|
|
fi
|
|
|
|
step_position_backward row col
|
|
|
|
while ((row > 0 || col > 0)); do
|
|
if ((col >= ${#lines[row]})); then
|
|
step_position_backward row col
|
|
continue
|
|
fi
|
|
char="${lines[row]:col:1}"
|
|
! is_blank_char "$char" && break
|
|
step_position_backward row col
|
|
done
|
|
|
|
cursorRow="$row"
|
|
cursorCol="$col"
|
|
done
|
|
}
|
|
|
|
move_to_first_nonblank() {
|
|
local line="${lines[cursorRow]}"
|
|
local col=0
|
|
while ((col < ${#line})); do
|
|
if ! is_blank_char "${line:col:1}"; then
|
|
break
|
|
fi
|
|
((col++))
|
|
done
|
|
cursorCol="$col"
|
|
}
|
|
|
|
move_to_line() {
|
|
local lineNumber="$1"
|
|
((lineNumber < 1)) && lineNumber=1
|
|
((lineNumber > ${#lines[@]})) && lineNumber="${#lines[@]}"
|
|
cursorRow=$((lineNumber - 1))
|
|
clamp_cursor
|
|
}
|
|
|
|
command_count() {
|
|
if [[ -n "$commandCount" ]]; then
|
|
printf '%s' "$commandCount"
|
|
else
|
|
printf '1'
|
|
fi
|
|
}
|
|
|
|
record_last_change() {
|
|
local keys="$1"
|
|
if ((replayKeysRemaining == 0)); then
|
|
lastChangeKeys="$keys"
|
|
fi
|
|
}
|
|
|
|
start_insert_change() {
|
|
local keys="$1"
|
|
if ((replayKeysRemaining == 0)); then
|
|
insertChangeKeys="$keys"
|
|
fi
|
|
}
|
|
|
|
finish_insert_change() {
|
|
if ((replayKeysRemaining == 0 && ${#insertChangeKeys} > 0)); then
|
|
lastChangeKeys="${insertChangeKeys}"$'\033'
|
|
fi
|
|
insertChangeKeys=""
|
|
}
|
|
|
|
repeat_last_change() {
|
|
local count="$1"
|
|
local repeatedKeys=""
|
|
local i
|
|
if [[ -z "$lastChangeKeys" ]]; then
|
|
statusMessage="No change to repeat"
|
|
return
|
|
fi
|
|
|
|
for ((i = 0; i < count; i++)); do
|
|
repeatedKeys+="$lastChangeKeys"
|
|
done
|
|
pendingInput="${repeatedKeys}${pendingInput}"
|
|
replayKeysRemaining=$((replayKeysRemaining + ${#repeatedKeys}))
|
|
statusMessage=""
|
|
}
|
|
|
|
bim_command_exists() {
|
|
local name="$1"
|
|
[[ -n "${bimCommands[$name]+set}" ]]
|
|
}
|
|
|
|
bim_command_has_prefix() {
|
|
local prefix="$1"
|
|
local name
|
|
for name in "${!bimCommands[@]}"; do
|
|
[[ "$name" == "$prefix"* ]] && return 0
|
|
done
|
|
return 1
|
|
}
|
|
|
|
run_bim_command() {
|
|
local name="$1"
|
|
local commandText="${bimCommands[$name]}"
|
|
local output exitStatus
|
|
|
|
output="$(bash -c "$commandText" </dev/null 2>/dev/null)"
|
|
exitStatus=$?
|
|
if ((exitStatus != 0)); then
|
|
statusMessage=",$name failed with status $exitStatus"
|
|
return
|
|
fi
|
|
if [[ -z "$output" ]]; then
|
|
statusMessage=",$name produced no output"
|
|
return
|
|
fi
|
|
|
|
insert_text_at_cursor "${output} "
|
|
statusMessage="Inserted ,$name"
|
|
}
|
|
|
|
handle_bim_command_key() {
|
|
local key="$1"
|
|
|
|
case "$key" in
|
|
$'\033')
|
|
bimCommandActive=0
|
|
bimCommandName=""
|
|
statusMessage=""
|
|
return
|
|
;;
|
|
$'\r'|$'\n')
|
|
if bim_command_exists "$bimCommandName"; then
|
|
run_bim_command "$bimCommandName"
|
|
else
|
|
statusMessage="Unknown command: ,$bimCommandName"
|
|
fi
|
|
bimCommandActive=0
|
|
bimCommandName=""
|
|
return
|
|
;;
|
|
$'\177'|$'\b')
|
|
if ((${#bimCommandName} > 0)); then
|
|
bimCommandName="${bimCommandName:0:${#bimCommandName}-1}"
|
|
fi
|
|
return
|
|
;;
|
|
esac
|
|
|
|
if [[ "$key" =~ ^[A-Za-z0-9_-]$ ]]; then
|
|
bimCommandName+="$key"
|
|
if ! bim_command_has_prefix "$bimCommandName"; then
|
|
statusMessage="Unknown command: ,$bimCommandName"
|
|
bimCommandActive=0
|
|
bimCommandName=""
|
|
fi
|
|
else
|
|
statusMessage="Invalid command name: ,$bimCommandName"
|
|
bimCommandActive=0
|
|
bimCommandName=""
|
|
fi
|
|
}
|
|
|
|
save_undo() {
|
|
undoLines=("${lines[@]}")
|
|
undoCursorRow="$cursorRow"
|
|
undoCursorCol="$cursorCol"
|
|
undoTopRow="$topRow"
|
|
undoDirty="$dirty"
|
|
hasUndo=1
|
|
}
|
|
|
|
ensure_insert_undo() {
|
|
if ((insertUndoSaved == 0)); then
|
|
save_undo
|
|
insertUndoSaved=1
|
|
fi
|
|
}
|
|
|
|
enter_insert_mode() {
|
|
local saveSnapshot="${1:-1}"
|
|
if ((saveSnapshot == 1)); then
|
|
save_undo
|
|
fi
|
|
insertUndoSaved=1
|
|
mode="insert"
|
|
statusMessage=""
|
|
}
|
|
|
|
undo_last_change() {
|
|
if ((hasUndo == 0)); then
|
|
statusMessage="Already at oldest change"
|
|
return
|
|
fi
|
|
|
|
lines=("${undoLines[@]}")
|
|
cursorRow="$undoCursorRow"
|
|
cursorCol="$undoCursorCol"
|
|
topRow="$undoTopRow"
|
|
dirty="$undoDirty"
|
|
hasUndo=0
|
|
insertUndoSaved=0
|
|
statusMessage="Undone"
|
|
clamp_cursor
|
|
}
|
|
|
|
insert_char() {
|
|
local char="$1"
|
|
local line="${lines[cursorRow]}"
|
|
ensure_insert_undo
|
|
lines[cursorRow]="${line:0:cursorCol}${char}${line:cursorCol}"
|
|
((cursorCol++))
|
|
dirty=1
|
|
}
|
|
|
|
split_line() {
|
|
local line="${lines[cursorRow]}"
|
|
local before="${line:0:cursorCol}"
|
|
local after="${line:cursorCol}"
|
|
ensure_insert_undo
|
|
lines[cursorRow]="$before"
|
|
lines=("${lines[@]:0:cursorRow+1}" "$after" "${lines[@]:cursorRow+1}")
|
|
((cursorRow++))
|
|
cursorCol=0
|
|
dirty=1
|
|
}
|
|
|
|
backspace_insert() {
|
|
local line previous
|
|
if ((cursorCol > 0)); then
|
|
ensure_insert_undo
|
|
line="${lines[cursorRow]}"
|
|
lines[cursorRow]="${line:0:cursorCol-1}${line:cursorCol}"
|
|
((cursorCol--))
|
|
dirty=1
|
|
elif ((cursorRow > 0)); then
|
|
ensure_insert_undo
|
|
previous="${lines[cursorRow-1]}"
|
|
cursorCol="${#previous}"
|
|
lines[cursorRow-1]+="${lines[cursorRow]}"
|
|
lines=("${lines[@]:0:cursorRow}" "${lines[@]:cursorRow+1}")
|
|
((cursorRow--))
|
|
dirty=1
|
|
fi
|
|
}
|
|
|
|
insert_text_at_cursor() {
|
|
local text="$1"
|
|
local line="${lines[cursorRow]}"
|
|
local prefix="${line:0:cursorCol}"
|
|
local suffix="${line:cursorCol}"
|
|
local rest="$text"
|
|
local segment
|
|
local -a parts=()
|
|
local i lastIndex
|
|
|
|
[[ -n "$text" ]] || return 1
|
|
|
|
while [[ "$rest" == *$'\n'* ]]; do
|
|
segment="${rest%%$'\n'*}"
|
|
parts+=("$segment")
|
|
rest="${rest#*$'\n'}"
|
|
done
|
|
parts+=("$rest")
|
|
|
|
save_undo
|
|
if ((${#parts[@]} == 1)); then
|
|
lines[cursorRow]="${prefix}${parts[0]}${suffix}"
|
|
cursorCol=$((cursorCol + ${#parts[0]}))
|
|
else
|
|
lastIndex=$((${#parts[@]} - 1))
|
|
parts[0]="${prefix}${parts[0]}"
|
|
parts[lastIndex]="${parts[lastIndex]}${suffix}"
|
|
lines=("${lines[@]:0:cursorRow}" "${parts[@]}" "${lines[@]:cursorRow+1}")
|
|
cursorRow=$((cursorRow + lastIndex))
|
|
cursorCol=$((${#parts[lastIndex]} - ${#suffix}))
|
|
fi
|
|
dirty=1
|
|
insertUndoSaved=0
|
|
clamp_cursor
|
|
}
|
|
|
|
delete_under_cursor() {
|
|
local line="${lines[cursorRow]}"
|
|
if ((cursorCol < ${#line})); then
|
|
lines[cursorRow]="${line:0:cursorCol}${line:cursorCol+1}"
|
|
dirty=1
|
|
fi
|
|
}
|
|
|
|
compare_positions() {
|
|
local leftRow="$1"
|
|
local leftCol="$2"
|
|
local rightRow="$3"
|
|
local rightCol="$4"
|
|
if ((leftRow < rightRow || (leftRow == rightRow && leftCol < rightCol))); then
|
|
printf -- '-1'
|
|
elif ((leftRow == rightRow && leftCol == rightCol)); then
|
|
printf '0'
|
|
else
|
|
printf '1'
|
|
fi
|
|
}
|
|
|
|
delete_range() {
|
|
local startRow="$1"
|
|
local startCol="$2"
|
|
local endRow="$3"
|
|
local endCol="$4"
|
|
local comparison line prefix suffix
|
|
|
|
comparison="$(compare_positions "$startRow" "$startCol" "$endRow" "$endCol")"
|
|
if [[ "$comparison" == "0" ]]; then
|
|
return
|
|
elif [[ "$comparison" == "1" ]]; then
|
|
local tempRow="$startRow"
|
|
local tempCol="$startCol"
|
|
startRow="$endRow"
|
|
startCol="$endCol"
|
|
endRow="$tempRow"
|
|
endCol="$tempCol"
|
|
fi
|
|
|
|
if ((startRow == endRow)); then
|
|
line="${lines[startRow]}"
|
|
lines[startRow]="${line:0:startCol}${line:endCol}"
|
|
else
|
|
prefix="${lines[startRow]:0:startCol}"
|
|
suffix="${lines[endRow]:endCol}"
|
|
lines=("${lines[@]:0:startRow}" "${prefix}${suffix}" "${lines[@]:endRow+1}")
|
|
fi
|
|
|
|
if ((${#lines[@]} == 0)); then
|
|
lines=("")
|
|
fi
|
|
cursorRow="$startRow"
|
|
cursorCol="$startCol"
|
|
dirty=1
|
|
statusMessage=""
|
|
clamp_cursor
|
|
}
|
|
|
|
line_end_for_operator() {
|
|
local row="$1"
|
|
printf '%s' "${#lines[row]}"
|
|
}
|
|
|
|
yank_range() {
|
|
local startRow="$1"
|
|
local startCol="$2"
|
|
local endRow="$3"
|
|
local endCol="$4"
|
|
local comparison row
|
|
|
|
comparison="$(compare_positions "$startRow" "$startCol" "$endRow" "$endCol")"
|
|
if [[ "$comparison" == "0" ]]; then
|
|
return 1
|
|
elif [[ "$comparison" == "1" ]]; then
|
|
local tempRow="$startRow"
|
|
local tempCol="$startCol"
|
|
startRow="$endRow"
|
|
startCol="$endCol"
|
|
endRow="$tempRow"
|
|
endCol="$tempCol"
|
|
fi
|
|
|
|
yankType="char"
|
|
if ((startRow == endRow)); then
|
|
yankBuffer=("${lines[startRow]:startCol:endCol-startCol}")
|
|
else
|
|
yankBuffer=("${lines[startRow]:startCol}")
|
|
for ((row = startRow + 1; row < endRow; row++)); do
|
|
yankBuffer+=("${lines[row]}")
|
|
done
|
|
yankBuffer+=("${lines[endRow]:0:endCol}")
|
|
fi
|
|
hasYank=1
|
|
}
|
|
|
|
delete_to_end_of_line() {
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
local endCol
|
|
endCol="${#lines[startRow]}"
|
|
if ((startCol >= endCol)); then
|
|
return 1
|
|
fi
|
|
|
|
delete_range "$startRow" "$startCol" "$startRow" "$endCol"
|
|
}
|
|
|
|
delete_word_forward() {
|
|
local count="$1"
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
save_undo
|
|
move_word_forward "$count"
|
|
yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
|
|
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
|
|
}
|
|
|
|
delete_word_backward() {
|
|
local count="$1"
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
save_undo
|
|
move_word_backward "$count"
|
|
yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
|
|
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
|
|
}
|
|
|
|
yank_word_forward() {
|
|
local count="$1"
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
move_word_forward "$count"
|
|
if yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"; then
|
|
statusMessage="text yanked"
|
|
else
|
|
statusMessage="Nothing to yank"
|
|
fi
|
|
cursorRow="$startRow"
|
|
cursorCol="$startCol"
|
|
}
|
|
|
|
yank_word_backward() {
|
|
local count="$1"
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
move_word_backward "$count"
|
|
if yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"; then
|
|
statusMessage="text yanked"
|
|
else
|
|
statusMessage="Nothing to yank"
|
|
fi
|
|
cursorRow="$startRow"
|
|
cursorCol="$startCol"
|
|
}
|
|
|
|
yank_to_end_of_line() {
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
local endCol
|
|
endCol="$(line_end_for_operator "$startRow")"
|
|
if yank_range "$startRow" "$startCol" "$startRow" "$endCol"; then
|
|
statusMessage="text yanked"
|
|
else
|
|
statusMessage="Nothing to yank"
|
|
fi
|
|
}
|
|
|
|
change_word() {
|
|
local count="$1"
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
local endRow endCol
|
|
save_undo
|
|
move_word_end_forward "$count"
|
|
endRow="$cursorRow"
|
|
endCol="$cursorCol"
|
|
if ! is_last_buffer_position "$endRow" "$endCol"; then
|
|
step_position_forward endRow endCol
|
|
elif ((endCol < ${#lines[endRow]})); then
|
|
((endCol++))
|
|
fi
|
|
delete_range "$startRow" "$startCol" "$endRow" "$endCol"
|
|
enter_insert_mode 0
|
|
}
|
|
|
|
change_current_line() {
|
|
save_undo
|
|
lines[cursorRow]=""
|
|
cursorCol=0
|
|
dirty=1
|
|
enter_insert_mode 0
|
|
}
|
|
|
|
change_to_end_of_line() {
|
|
save_undo
|
|
delete_to_end_of_line
|
|
enter_insert_mode 0
|
|
}
|
|
|
|
substitute_chars() {
|
|
local count="$1"
|
|
local startCol="$cursorCol"
|
|
local endCol=$((cursorCol + count))
|
|
local lineLength
|
|
lineLength="${#lines[cursorRow]}"
|
|
((endCol > lineLength)) && endCol="$lineLength"
|
|
save_undo
|
|
if ((startCol < endCol)); then
|
|
delete_range "$cursorRow" "$startCol" "$cursorRow" "$endCol"
|
|
fi
|
|
enter_insert_mode 0
|
|
}
|
|
|
|
replace_chars() {
|
|
local count="$1"
|
|
local replacement="$2"
|
|
local line="${lines[cursorRow]}"
|
|
local lineLength="${#line}"
|
|
local actual=0
|
|
local i targetCol
|
|
|
|
if [[ ! "$replacement" =~ [[:print:]] ]] || ((cursorCol >= lineLength)); then
|
|
statusMessage=""
|
|
return
|
|
fi
|
|
|
|
save_undo
|
|
for ((i = 0; i < count; i++)); do
|
|
targetCol=$((cursorCol + i))
|
|
((targetCol >= lineLength)) && break
|
|
line="${line:0:targetCol}${replacement}${line:targetCol+1}"
|
|
((actual++))
|
|
done
|
|
lines[cursorRow]="$line"
|
|
if ((actual > 0)); then
|
|
cursorCol=$((cursorCol + actual - 1))
|
|
fi
|
|
dirty=1
|
|
statusMessage=""
|
|
}
|
|
|
|
join_lines() {
|
|
local count="$1"
|
|
local i nextLine
|
|
if ((cursorRow + 1 >= ${#lines[@]})); then
|
|
return
|
|
fi
|
|
|
|
save_undo
|
|
for ((i = 0; i < count && cursorRow + 1 < ${#lines[@]}; i++)); do
|
|
nextLine="${lines[cursorRow+1]}"
|
|
lines[cursorRow]="${lines[cursorRow]} ${nextLine#"${nextLine%%[![:space:]]*}"}"
|
|
lines=("${lines[@]:0:cursorRow+1}" "${lines[@]:cursorRow+2}")
|
|
done
|
|
dirty=1
|
|
statusMessage=""
|
|
}
|
|
|
|
search_from_position() {
|
|
local pattern="$1"
|
|
local direction="$2"
|
|
local startRow="$3"
|
|
local startCol="$4"
|
|
local offset row col line searchPart matchPrefix
|
|
|
|
if [[ -z "$pattern" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
for ((offset = 0; offset < ${#lines[@]}; offset++)); do
|
|
if [[ "$direction" == "forward" ]]; then
|
|
row=$(( (startRow + offset) % ${#lines[@]} ))
|
|
col=0
|
|
((offset == 0)) && col="$startCol"
|
|
else
|
|
row=$(( (startRow - offset + ${#lines[@]}) % ${#lines[@]} ))
|
|
col="${#lines[row]}"
|
|
((offset == 0)) && col="$startCol"
|
|
fi
|
|
|
|
line="${lines[row]}"
|
|
if [[ "$direction" == "forward" ]]; then
|
|
((col < 0)) && col=0
|
|
((col > ${#line})) && col="${#line}"
|
|
searchPart="${line:col}"
|
|
if [[ "$searchPart" == *"$pattern"* ]]; then
|
|
matchPrefix="${searchPart%%"$pattern"*}"
|
|
cursorRow="$row"
|
|
cursorCol=$((col + ${#matchPrefix}))
|
|
((offset > 0 && row <= startRow)) && statusMessage="search wrapped"
|
|
return 0
|
|
fi
|
|
else
|
|
((col < 0)) && col=0
|
|
((col > ${#line})) && col="${#line}"
|
|
searchPart="${line:0:col}"
|
|
if [[ "$searchPart" == *"$pattern"* ]]; then
|
|
matchPrefix="${searchPart%"$pattern"*}"
|
|
cursorRow="$row"
|
|
cursorCol="${#matchPrefix}"
|
|
((offset > 0 && row >= startRow)) && statusMessage="search wrapped"
|
|
return 0
|
|
fi
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
lastSearchPattern=""
|
|
lastSearchDirection="forward"
|
|
|
|
run_search() {
|
|
local pattern="$1"
|
|
local direction="$2"
|
|
local startRow="$cursorRow"
|
|
local startCol="$cursorCol"
|
|
|
|
if [[ "$direction" == "forward" ]]; then
|
|
((startCol++))
|
|
else
|
|
((startCol--))
|
|
fi
|
|
|
|
if search_from_position "$pattern" "$direction" "$startRow" "$startCol"; then
|
|
lastSearchPattern="$pattern"
|
|
lastSearchDirection="$direction"
|
|
[[ "$statusMessage" == "search wrapped" ]] || statusMessage=""
|
|
clamp_cursor
|
|
else
|
|
statusMessage="Pattern not found: $pattern"
|
|
fi
|
|
}
|
|
|
|
repeat_search() {
|
|
local direction="$1"
|
|
if [[ -z "$lastSearchPattern" ]]; then
|
|
statusMessage="No previous search"
|
|
return
|
|
fi
|
|
run_search "$lastSearchPattern" "$direction"
|
|
}
|
|
|
|
prompt_search() {
|
|
local direction="$1"
|
|
local prompt key pattern
|
|
if [[ "$direction" == "forward" ]]; then
|
|
prompt="/"
|
|
else
|
|
prompt="?"
|
|
fi
|
|
pattern="$prompt"
|
|
while true; do
|
|
statusMessage="$pattern"
|
|
draw_screen
|
|
read_key || return
|
|
case "$key" in
|
|
$'\033')
|
|
statusMessage=""
|
|
return
|
|
;;
|
|
$'\r'|$'\n')
|
|
break
|
|
;;
|
|
$'\177'|$'\b')
|
|
if ((${#pattern} > 1)); then
|
|
pattern="${pattern:0:${#pattern}-1}"
|
|
fi
|
|
;;
|
|
*)
|
|
if [[ "$key" =~ [[:print:]] ]]; then
|
|
pattern+="$key"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
pattern="${pattern#"$prompt"}"
|
|
if [[ -z "$pattern" ]]; then
|
|
statusMessage="Empty search"
|
|
return
|
|
fi
|
|
run_search "$pattern" "$direction"
|
|
}
|
|
|
|
open_line_below() {
|
|
lines=("${lines[@]:0:cursorRow+1}" "" "${lines[@]:cursorRow+1}")
|
|
((cursorRow++))
|
|
cursorCol=0
|
|
enter_insert_mode 0
|
|
dirty=1
|
|
}
|
|
|
|
open_line_above() {
|
|
lines=("${lines[@]:0:cursorRow}" "" "${lines[@]:cursorRow}")
|
|
cursorCol=0
|
|
enter_insert_mode 0
|
|
dirty=1
|
|
}
|
|
|
|
yank_lines() {
|
|
local requested="$1"
|
|
local available=$(( ${#lines[@]} - cursorRow ))
|
|
local actual="$requested"
|
|
((actual > available)) && actual="$available"
|
|
((actual < 1)) && actual=1
|
|
|
|
yankType="line"
|
|
yankBuffer=("${lines[@]:cursorRow:actual}")
|
|
hasYank=1
|
|
statusMessage="$actual line yanked"
|
|
((actual != 1)) && statusMessage="$actual lines yanked"
|
|
}
|
|
|
|
delete_lines() {
|
|
local requested="$1"
|
|
local available=$(( ${#lines[@]} - cursorRow ))
|
|
local actual="$requested"
|
|
((actual > available)) && actual="$available"
|
|
((actual < 1)) && actual=1
|
|
|
|
save_undo
|
|
yankType="line"
|
|
yankBuffer=("${lines[@]:cursorRow:actual}")
|
|
hasYank=1
|
|
|
|
lines=("${lines[@]:0:cursorRow}" "${lines[@]:cursorRow+actual}")
|
|
if ((${#lines[@]} == 0)); then
|
|
lines=("")
|
|
cursorRow=0
|
|
elif ((cursorRow >= ${#lines[@]})); then
|
|
cursorRow=$(( ${#lines[@]} - 1 ))
|
|
fi
|
|
cursorCol=0
|
|
statusMessage="$actual line deleted"
|
|
((actual != 1)) && statusMessage="$actual lines deleted"
|
|
dirty=1
|
|
}
|
|
|
|
paste_line_below() {
|
|
local count="$1"
|
|
local insertAt=$((cursorRow + 1))
|
|
local firstPastedRow="$insertAt"
|
|
|
|
if ((hasYank == 0)); then
|
|
statusMessage="Nothing in yank buffer"
|
|
return
|
|
fi
|
|
|
|
save_undo
|
|
local i
|
|
for ((i = 0; i < count; i++)); do
|
|
lines=("${lines[@]:0:insertAt}" "${yankBuffer[@]}" "${lines[@]:insertAt}")
|
|
insertAt=$((insertAt + ${#yankBuffer[@]}))
|
|
done
|
|
cursorRow="$firstPastedRow"
|
|
cursorCol=0
|
|
statusMessage=""
|
|
dirty=1
|
|
}
|
|
|
|
paste_char_after_cursor() {
|
|
local count="$1"
|
|
local pasteRow="$cursorRow"
|
|
local pasteCol="$cursorCol"
|
|
local line prefix suffix i insertAt
|
|
|
|
if ((hasYank == 0)); then
|
|
statusMessage="Nothing in yank buffer"
|
|
return
|
|
fi
|
|
|
|
if [[ "$yankType" == "line" ]]; then
|
|
paste_line_below "$count"
|
|
return
|
|
fi
|
|
|
|
save_undo
|
|
if ((pasteCol < ${#lines[pasteRow]})); then
|
|
((pasteCol++))
|
|
fi
|
|
|
|
for ((i = 0; i < count; i++)); do
|
|
line="${lines[pasteRow]}"
|
|
prefix="${line:0:pasteCol}"
|
|
suffix="${line:pasteCol}"
|
|
if ((${#yankBuffer[@]} == 1)); then
|
|
lines[pasteRow]="${prefix}${yankBuffer[0]}${suffix}"
|
|
cursorRow="$pasteRow"
|
|
cursorCol=$((pasteCol + ${#yankBuffer[0]} - 1))
|
|
pasteCol=$((cursorCol + 1))
|
|
else
|
|
lines[pasteRow]="${prefix}${yankBuffer[0]}"
|
|
insertAt=$((pasteRow + 1))
|
|
if ((${#yankBuffer[@]} > 2)); then
|
|
lines=("${lines[@]:0:insertAt}" "${yankBuffer[@]:1:${#yankBuffer[@]}-2}" "${lines[@]:insertAt}")
|
|
insertAt=$((insertAt + ${#yankBuffer[@]} - 2))
|
|
fi
|
|
lines=("${lines[@]:0:insertAt}" "${yankBuffer[-1]}${suffix}" "${lines[@]:insertAt}")
|
|
cursorRow="$insertAt"
|
|
cursorCol=$(( ${#yankBuffer[-1]} - 1 ))
|
|
pasteRow="$cursorRow"
|
|
pasteCol=$((cursorCol + 1))
|
|
fi
|
|
done
|
|
dirty=1
|
|
statusMessage=""
|
|
clamp_cursor
|
|
}
|
|
|
|
save_file() {
|
|
local target="$1"
|
|
[[ -z "$target" ]] && return 1
|
|
|
|
local line
|
|
: > "$target"
|
|
for line in "${lines[@]}"; do
|
|
printf '%s\n' "$line" >> "$target"
|
|
done
|
|
filePath="$target"
|
|
dirty=0
|
|
statusMessage="Wrote $target"
|
|
}
|
|
|
|
prompt_command() {
|
|
local command key
|
|
command=":"
|
|
while true; do
|
|
statusMessage="$command"
|
|
draw_screen
|
|
read_key || return
|
|
case "$key" in
|
|
$'\033')
|
|
statusMessage=""
|
|
return
|
|
;;
|
|
$'\r'|$'\n')
|
|
break
|
|
;;
|
|
$'\177'|$'\b')
|
|
if ((${#command} > 1)); then
|
|
command="${command:0:${#command}-1}"
|
|
fi
|
|
;;
|
|
*)
|
|
if [[ "$key" =~ [[:print:]] ]]; then
|
|
command+="$key"
|
|
fi
|
|
;;
|
|
esac
|
|
done
|
|
|
|
command="${command#:}"
|
|
case "$command" in
|
|
q)
|
|
if [[ "$dirty" == 1 ]]; then
|
|
statusMessage="Unsaved changes. Use :q! to quit anyway."
|
|
else
|
|
exit 0
|
|
fi
|
|
;;
|
|
q!)
|
|
exit 0
|
|
;;
|
|
w)
|
|
if save_file "$filePath"; then
|
|
:
|
|
else
|
|
statusMessage="No file name. Use :w path"
|
|
fi
|
|
;;
|
|
w\ *)
|
|
save_file "${command#w }" || statusMessage="Could not write file"
|
|
;;
|
|
wq)
|
|
if save_file "$filePath"; then
|
|
exit 0
|
|
else
|
|
statusMessage="No file name. Use :w path"
|
|
fi
|
|
;;
|
|
wq\ *)
|
|
save_file "${command#wq }" && exit 0
|
|
statusMessage="Could not write file"
|
|
;;
|
|
*)
|
|
statusMessage="Unknown command: :$command"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
handle_command_key() {
|
|
local key="$1"
|
|
local count i
|
|
if ((bimCommandActive)); then
|
|
handle_bim_command_key "$key"
|
|
commandCount=""
|
|
pendingCommand=""
|
|
return
|
|
fi
|
|
|
|
if [[ -n "$pendingCommand" ]]; then
|
|
count="$(command_count)"
|
|
if [[ "$pendingCommand" == "r" ]]; then
|
|
record_last_change "${commandCount}${pendingCommand}${key}"
|
|
replace_chars "$count" "$key"
|
|
pendingCommand=""
|
|
commandCount=""
|
|
return
|
|
fi
|
|
case "${pendingCommand}${key}" in
|
|
dd)
|
|
record_last_change "${commandCount}${pendingCommand}${key}"
|
|
delete_lines "$count"
|
|
;;
|
|
d'$')
|
|
record_last_change "${commandCount}${pendingCommand}${key}"
|
|
save_undo
|
|
yank_to_end_of_line
|
|
delete_to_end_of_line
|
|
;;
|
|
dw)
|
|
record_last_change "${commandCount}${pendingCommand}${key}"
|
|
delete_word_forward "$count"
|
|
;;
|
|
db)
|
|
record_last_change "${commandCount}${pendingCommand}${key}"
|
|
delete_word_backward "$count"
|
|
;;
|
|
y'$')
|
|
yank_to_end_of_line
|
|
;;
|
|
yw)
|
|
yank_word_forward "$count"
|
|
;;
|
|
yb)
|
|
yank_word_backward "$count"
|
|
;;
|
|
cc)
|
|
start_insert_change "${commandCount}${pendingCommand}${key}"
|
|
change_current_line
|
|
;;
|
|
c'$')
|
|
start_insert_change "${commandCount}${pendingCommand}${key}"
|
|
change_to_end_of_line
|
|
;;
|
|
cw)
|
|
start_insert_change "${commandCount}${pendingCommand}${key}"
|
|
change_word "$count"
|
|
;;
|
|
ge)
|
|
move_word_end_backward "$count"
|
|
;;
|
|
gg)
|
|
if [[ -n "$commandCount" ]]; then
|
|
move_to_line "$commandCount"
|
|
else
|
|
move_to_line 1
|
|
fi
|
|
;;
|
|
yy)
|
|
yank_lines "$count"
|
|
;;
|
|
*)
|
|
statusMessage=""
|
|
;;
|
|
esac
|
|
pendingCommand=""
|
|
commandCount=""
|
|
return
|
|
fi
|
|
|
|
case "$key" in
|
|
[1-9])
|
|
commandCount+="$key"
|
|
statusMessage=""
|
|
;;
|
|
0)
|
|
if [[ -n "$commandCount" ]]; then
|
|
commandCount+="$key"
|
|
statusMessage=""
|
|
else
|
|
cursorCol=0
|
|
fi
|
|
;;
|
|
i)
|
|
start_insert_change "${commandCount}${key}"
|
|
commandCount=""
|
|
enter_insert_mode
|
|
;;
|
|
a)
|
|
start_insert_change "${commandCount}${key}"
|
|
commandCount=""
|
|
if ((cursorCol < $(line_length "$cursorRow"))); then
|
|
((cursorCol++))
|
|
fi
|
|
enter_insert_mode
|
|
;;
|
|
I)
|
|
start_insert_change "${commandCount}${key}"
|
|
commandCount=""
|
|
cursorCol=0
|
|
enter_insert_mode
|
|
;;
|
|
A)
|
|
start_insert_change "${commandCount}${key}"
|
|
commandCount=""
|
|
cursorCol="$(line_length "$cursorRow")"
|
|
enter_insert_mode
|
|
;;
|
|
h|$'\033[D')
|
|
count="$(command_count)"
|
|
for ((i = 0; i < count; i++)); do move_left; done
|
|
commandCount=""
|
|
;;
|
|
l|$'\033[C')
|
|
count="$(command_count)"
|
|
for ((i = 0; i < count; i++)); do move_right; done
|
|
commandCount=""
|
|
;;
|
|
k|$'\033[A')
|
|
count="$(command_count)"
|
|
for ((i = 0; i < count; i++)); do move_up; done
|
|
commandCount=""
|
|
;;
|
|
j|$'\033[B')
|
|
count="$(command_count)"
|
|
for ((i = 0; i < count; i++)); do move_down; done
|
|
commandCount=""
|
|
;;
|
|
w)
|
|
count="$(command_count)"
|
|
move_word_forward "$count"
|
|
commandCount=""
|
|
;;
|
|
e)
|
|
count="$(command_count)"
|
|
move_word_end_forward "$count"
|
|
commandCount=""
|
|
;;
|
|
b)
|
|
count="$(command_count)"
|
|
move_word_backward "$count"
|
|
commandCount=""
|
|
;;
|
|
G)
|
|
if [[ -n "$commandCount" ]]; then
|
|
move_to_line "$commandCount"
|
|
else
|
|
move_to_line "${#lines[@]}"
|
|
fi
|
|
commandCount=""
|
|
;;
|
|
'^')
|
|
commandCount=""
|
|
move_to_first_nonblank
|
|
;;
|
|
'$')
|
|
commandCount=""
|
|
cursorCol="$(line_length "$cursorRow")"
|
|
;;
|
|
x)
|
|
count="$(command_count)"
|
|
record_last_change "${commandCount}${key}"
|
|
save_undo
|
|
yank_range "$cursorRow" "$cursorCol" "$cursorRow" "$((cursorCol + count))"
|
|
for ((i = 0; i < count; i++)); do delete_under_cursor; done
|
|
commandCount=""
|
|
;;
|
|
D)
|
|
record_last_change "${commandCount}${key}"
|
|
commandCount=""
|
|
save_undo
|
|
yank_to_end_of_line
|
|
delete_to_end_of_line
|
|
;;
|
|
C)
|
|
start_insert_change "${commandCount}${key}"
|
|
commandCount=""
|
|
change_to_end_of_line
|
|
;;
|
|
s)
|
|
count="$(command_count)"
|
|
start_insert_change "${commandCount}${key}"
|
|
yank_range "$cursorRow" "$cursorCol" "$cursorRow" "$((cursorCol + count))"
|
|
substitute_chars "$count"
|
|
commandCount=""
|
|
;;
|
|
r)
|
|
pendingCommand="$key"
|
|
statusMessage=""
|
|
;;
|
|
d|y|c|g)
|
|
pendingCommand="$key"
|
|
statusMessage=""
|
|
;;
|
|
p)
|
|
count="$(command_count)"
|
|
record_last_change "${commandCount}${key}"
|
|
paste_char_after_cursor "$count"
|
|
commandCount=""
|
|
;;
|
|
J)
|
|
count="$(command_count)"
|
|
record_last_change "${commandCount}${key}"
|
|
join_lines "$count"
|
|
commandCount=""
|
|
;;
|
|
o)
|
|
count="$(command_count)"
|
|
start_insert_change "${commandCount}${key}"
|
|
save_undo
|
|
for ((i = 0; i < count; i++)); do open_line_below; done
|
|
commandCount=""
|
|
;;
|
|
O)
|
|
count="$(command_count)"
|
|
start_insert_change "${commandCount}${key}"
|
|
save_undo
|
|
for ((i = 0; i < count; i++)); do open_line_above; done
|
|
commandCount=""
|
|
;;
|
|
/)
|
|
commandCount=""
|
|
prompt_search forward
|
|
;;
|
|
'?')
|
|
commandCount=""
|
|
prompt_search backward
|
|
;;
|
|
n)
|
|
commandCount=""
|
|
repeat_search "$lastSearchDirection"
|
|
;;
|
|
N)
|
|
commandCount=""
|
|
if [[ "$lastSearchDirection" == "forward" ]]; then
|
|
repeat_search backward
|
|
else
|
|
repeat_search forward
|
|
fi
|
|
;;
|
|
'.')
|
|
count="$(command_count)"
|
|
commandCount=""
|
|
repeat_last_change "$count"
|
|
;;
|
|
:)
|
|
commandCount=""
|
|
prompt_command
|
|
;;
|
|
,)
|
|
commandCount=""
|
|
if ((${#bimCommands[@]} == 0)); then
|
|
statusMessage="No ~/.bimrc commands"
|
|
else
|
|
bimCommandActive=1
|
|
bimCommandName=""
|
|
statusMessage=""
|
|
fi
|
|
;;
|
|
$'\007')
|
|
commandCount=""
|
|
file_info
|
|
;;
|
|
u)
|
|
commandCount=""
|
|
undo_last_change
|
|
;;
|
|
$'\003')
|
|
commandCount=""
|
|
statusMessage="Use :q to quit"
|
|
;;
|
|
*)
|
|
commandCount=""
|
|
;;
|
|
esac
|
|
}
|
|
|
|
handle_insert_key() {
|
|
local key="$1"
|
|
case "$key" in
|
|
$'\033')
|
|
finish_insert_change
|
|
mode="command"
|
|
insertUndoSaved=0
|
|
statusMessage=""
|
|
;;
|
|
$'\033[D')
|
|
insertChangeKeys+="$key"
|
|
move_left
|
|
;;
|
|
$'\033[C')
|
|
insertChangeKeys+="$key"
|
|
move_right
|
|
;;
|
|
$'\033[A')
|
|
insertChangeKeys+="$key"
|
|
move_up
|
|
;;
|
|
$'\033[B')
|
|
insertChangeKeys+="$key"
|
|
move_down
|
|
;;
|
|
$'\r'|$'\n')
|
|
insertChangeKeys+="$key"
|
|
split_line
|
|
;;
|
|
$'\177'|$'\b')
|
|
insertChangeKeys+="$key"
|
|
backspace_insert
|
|
;;
|
|
$'\007')
|
|
file_info
|
|
;;
|
|
$'\003')
|
|
finish_insert_change
|
|
mode="command"
|
|
insertUndoSaved=0
|
|
statusMessage="Use :q to quit"
|
|
;;
|
|
*)
|
|
if [[ "$key" =~ [[:print:]] ]]; then
|
|
insertChangeKeys+="$key"
|
|
insert_char "$key"
|
|
fi
|
|
;;
|
|
esac
|
|
}
|
|
|
|
while true; do
|
|
draw_screen
|
|
read_key || exit 0
|
|
if [[ "$mode" == "insert" ]]; then
|
|
handle_insert_key "$key"
|
|
else
|
|
handle_command_key "$key"
|
|
fi
|
|
done
|