Instead of saying something boring like initial commit... Behold, evil and darkness unleashed upon the world... That's right, vim in bash!
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# bim
|
||||
|
||||
`bim` is a tiny Vim-like editor written in Bash.
|
||||
|
||||
This is an early prototype, not a full Vim replacement. It starts in command
|
||||
mode and supports:
|
||||
|
||||
- `i` insert before the cursor
|
||||
- `a` append after the cursor
|
||||
- `I` insert at the beginning of the line
|
||||
- `A` append at the end of the line
|
||||
- arrow keys, plus `h`, `j`, `k`, `l`
|
||||
- `w` move forward by word
|
||||
- `b` move backward by word
|
||||
- `e` move to the end of the current or next word
|
||||
- `x` delete the character under the cursor
|
||||
- `dd` delete the current line
|
||||
- `d$` or `D` delete from the cursor to the end of the line
|
||||
- `dw` delete forward by word
|
||||
- `db` delete backward by word
|
||||
- `cw` change word
|
||||
- `cc` change the current line
|
||||
- `C` change from the cursor to the end of the line
|
||||
- `s` substitute the character under the cursor
|
||||
- `r` replace the character under the cursor
|
||||
- `yy` yank the current line
|
||||
- `p` paste the yanked or deleted line below the cursor
|
||||
- `o` open a new line below and enter insert mode
|
||||
- `O` open a new line above and enter insert mode
|
||||
- `u` undo the last change
|
||||
- numeric prefixes for common commands, such as `2dd`, `20yy`, `3p`, `5j`,
|
||||
`4x`, `5w`, `5dw`, `3rX`, and `2s`
|
||||
- `Esc` return to command mode
|
||||
- `Ctrl+G` show file, modified state, line, and column info
|
||||
- `:w`, `:w path`, `:q`, `:q!`, `:wq`, and `:wq path`
|
||||
|
||||
There is no persistent ruler/status line. The bottom line is used only for
|
||||
commands, messages, and explicit `Ctrl+G` file info.
|
||||
|
||||
Run it with:
|
||||
|
||||
```bash
|
||||
./bim path/to/file
|
||||
```
|
||||
@@ -0,0 +1,989 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
filePath="${1:-}"
|
||||
mode="command"
|
||||
statusMessage=""
|
||||
renderedMessage=$'\001'
|
||||
pendingInput=""
|
||||
pendingCommand=""
|
||||
commandCount=""
|
||||
hasYank=0
|
||||
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
|
||||
|
||||
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'
|
||||
|
||||
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() {
|
||||
if [[ "$statusMessage" != "$renderedMessage" ]]; then
|
||||
printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$statusMessage"
|
||||
renderedMessage="$statusMessage"
|
||||
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
|
||||
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}"
|
||||
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
|
||||
}
|
||||
|
||||
command_count() {
|
||||
if [[ -n "$commandCount" ]]; then
|
||||
printf '%s' "$commandCount"
|
||||
else
|
||||
printf '1'
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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"
|
||||
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
|
||||
}
|
||||
|
||||
delete_word_backward() {
|
||||
local count="$1"
|
||||
local startRow="$cursorRow"
|
||||
local startCol="$cursorCol"
|
||||
save_undo
|
||||
move_word_backward "$count"
|
||||
delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol"
|
||||
}
|
||||
|
||||
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=""
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
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 [[ -n "$pendingCommand" ]]; then
|
||||
count="$(command_count)"
|
||||
if [[ "$pendingCommand" == "r" ]]; then
|
||||
replace_chars "$count" "$key"
|
||||
pendingCommand=""
|
||||
commandCount=""
|
||||
return
|
||||
fi
|
||||
case "${pendingCommand}${key}" in
|
||||
dd)
|
||||
delete_lines "$count"
|
||||
;;
|
||||
d'$')
|
||||
save_undo
|
||||
delete_to_end_of_line
|
||||
;;
|
||||
dw)
|
||||
delete_word_forward "$count"
|
||||
;;
|
||||
db)
|
||||
delete_word_backward "$count"
|
||||
;;
|
||||
cc)
|
||||
change_current_line
|
||||
;;
|
||||
c'$')
|
||||
change_to_end_of_line
|
||||
;;
|
||||
cw)
|
||||
change_word "$count"
|
||||
;;
|
||||
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)
|
||||
commandCount=""
|
||||
enter_insert_mode
|
||||
;;
|
||||
a)
|
||||
commandCount=""
|
||||
if ((cursorCol < $(line_length "$cursorRow"))); then
|
||||
((cursorCol++))
|
||||
fi
|
||||
enter_insert_mode
|
||||
;;
|
||||
I)
|
||||
commandCount=""
|
||||
cursorCol=0
|
||||
enter_insert_mode
|
||||
;;
|
||||
A)
|
||||
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=""
|
||||
;;
|
||||
'$')
|
||||
commandCount=""
|
||||
cursorCol="$(line_length "$cursorRow")"
|
||||
;;
|
||||
x)
|
||||
count="$(command_count)"
|
||||
save_undo
|
||||
for ((i = 0; i < count; i++)); do delete_under_cursor; done
|
||||
commandCount=""
|
||||
;;
|
||||
D)
|
||||
commandCount=""
|
||||
save_undo
|
||||
delete_to_end_of_line
|
||||
;;
|
||||
C)
|
||||
commandCount=""
|
||||
change_to_end_of_line
|
||||
;;
|
||||
s)
|
||||
count="$(command_count)"
|
||||
substitute_chars "$count"
|
||||
commandCount=""
|
||||
;;
|
||||
r)
|
||||
pendingCommand="$key"
|
||||
statusMessage=""
|
||||
;;
|
||||
d|y|c)
|
||||
pendingCommand="$key"
|
||||
statusMessage=""
|
||||
;;
|
||||
p)
|
||||
count="$(command_count)"
|
||||
paste_line_below "$count"
|
||||
commandCount=""
|
||||
;;
|
||||
o)
|
||||
count="$(command_count)"
|
||||
save_undo
|
||||
for ((i = 0; i < count; i++)); do open_line_below; done
|
||||
commandCount=""
|
||||
;;
|
||||
O)
|
||||
count="$(command_count)"
|
||||
save_undo
|
||||
for ((i = 0; i < count; i++)); do open_line_above; done
|
||||
commandCount=""
|
||||
;;
|
||||
:)
|
||||
commandCount=""
|
||||
prompt_command
|
||||
;;
|
||||
$'\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')
|
||||
mode="command"
|
||||
insertUndoSaved=0
|
||||
statusMessage=""
|
||||
;;
|
||||
$'\033[D')
|
||||
move_left
|
||||
;;
|
||||
$'\033[C')
|
||||
move_right
|
||||
;;
|
||||
$'\033[A')
|
||||
move_up
|
||||
;;
|
||||
$'\033[B')
|
||||
move_down
|
||||
;;
|
||||
$'\r'|$'\n')
|
||||
split_line
|
||||
;;
|
||||
$'\177'|$'\b')
|
||||
backspace_insert
|
||||
;;
|
||||
$'\007')
|
||||
file_info
|
||||
;;
|
||||
$'\003')
|
||||
mode="command"
|
||||
insertUndoSaved=0
|
||||
statusMessage="Use :q to quit"
|
||||
;;
|
||||
*)
|
||||
if [[ "$key" =~ [[:print:]] ]]; then
|
||||
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
|
||||
Reference in New Issue
Block a user