diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ee7d6a5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/bim b/bim index 6024a85..8a48851 100755 --- a/bim +++ b/bim @@ -1,6 +1,13 @@ #!/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="" @@ -8,7 +15,11 @@ renderedMessage=$'\001' pendingInput="" pendingCommand="" commandCount="" +lastChangeKeys="" +insertChangeKeys="" +replayKeysRemaining=0 hasYank=0 +yankType="line" dirty=0 cursorRow=0 cursorCol=0 @@ -85,9 +96,14 @@ file_info() { } draw_message() { - if [[ "$statusMessage" != "$renderedMessage" ]]; then - printf '\033[%s;1H%-*.*s' "$screenRows" "$screenCols" "$screenCols" "$statusMessage" - renderedMessage="$statusMessage" + local message="$statusMessage" + if [[ -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 } @@ -109,6 +125,9 @@ draw_screen() { 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))" } @@ -117,6 +136,7 @@ read_key() { if [[ -n "$pendingInput" ]]; then key="${pendingInput:0:1}" pendingInput="${pendingInput:1}" + ((replayKeysRemaining > 0)) && ((replayKeysRemaining--)) return 0 fi @@ -340,6 +360,53 @@ move_word_end_forward() { 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" @@ -348,6 +415,44 @@ command_count() { 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="" +} + save_undo() { undoLines=("${lines[@]}") undoCursorRow="$cursorRow" @@ -491,6 +596,43 @@ delete_range() { 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" @@ -509,6 +651,7 @@ delete_word_forward() { local startCol="$cursorCol" save_undo move_word_forward "$count" + yank_range "$startRow" "$startCol" "$cursorRow" "$cursorCol" delete_range "$startRow" "$startCol" "$cursorRow" "$cursorCol" } @@ -518,9 +661,50 @@ delete_word_backward() { 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" @@ -595,6 +779,150 @@ replace_chars() { 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++)) @@ -617,6 +945,7 @@ yank_lines() { ((actual > available)) && actual="$available" ((actual < 1)) && actual=1 + yankType="line" yankBuffer=("${lines[@]:cursorRow:actual}") hasYank=1 statusMessage="$actual line yanked" @@ -631,6 +960,7 @@ delete_lines() { ((actual < 1)) && actual=1 save_undo + yankType="line" yankBuffer=("${lines[@]:cursorRow:actual}") hasYank=1 @@ -669,6 +999,55 @@ paste_line_below() { 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 @@ -756,6 +1135,7 @@ handle_command_key() { if [[ -n "$pendingCommand" ]]; then count="$(command_count)" if [[ "$pendingCommand" == "r" ]]; then + record_last_change "${commandCount}${pendingCommand}${key}" replace_chars "$count" "$key" pendingCommand="" commandCount="" @@ -763,27 +1143,54 @@ handle_command_key() { 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" ;; @@ -810,10 +1217,12 @@ handle_command_key() { 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++)) @@ -821,11 +1230,13 @@ handle_command_key() { 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 @@ -865,27 +1276,46 @@ handle_command_key() { 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="" ;; @@ -893,27 +1323,61 @@ handle_command_key() { pendingCommand="$key" statusMessage="" ;; - d|y|c) + d|y|c|g) pendingCommand="$key" statusMessage="" ;; p) count="$(command_count)" - paste_line_below "$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 @@ -940,38 +1404,47 @@ 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 ;;