diff -uNr a/yrc/codemap.txt b/yrc/codemap.txt --- a/yrc/codemap.txt e08134a15620d6666db39851a414802e45319626b0e321af9c8df72bec0e604db9f668b085a4d603aa00d953c8ee1887b8818c5275ba4849e48aae005fb52416 +++ b/yrc/codemap.txt 2782c85d3fb82c2633b790aa0d6ff05fe4357652015cbdd6c90b0f31354ee20b2c21d3ec696968730c95390abbe7fb4033da19142eb4b0f2edd890cc7130843d @@ -154,8 +154,11 @@ pad_or_clip_str(str, width: int, pad=' ': chr) -> str wrap(line, width, indent=0) -> list of str is_digit(chr) -> bool +is_alpha(chr) -> bool parse_address(addr) -> (host: str, port: int) / ValueError format_address((host: str, port: int)) -> str +rfind_word_start(chars: str | list of char, start_cursor: int) -> int +find_word_end(chars: str | list of char, start_cursor: int) -> int int_of_bytes(str) -> int bit_length(int) -> int heap_peek(heap: list) -> (key, value) / IndexError @@ -229,8 +232,7 @@ history_next prompt_backspace prompt_delete -kill_start -kill_end +kill_range(int, int) yank yank_next info(msg: str, buf=None) : buf_log_msg to buf or buffers[0] diff -uNr a/yrc/manifest b/yrc/manifest --- a/yrc/manifest 688a80fbeae2c1065d9416d59bf3a0109f7a17c1ee3d2a0672769cb8e06844eaf291d067dcbc80208c8fa1b54c2b0bf3793c9e3b4bd15f4e372197df62acf8a6 +++ b/yrc/manifest 5d217f47cc4ceb39949a9e746040db7faa78ba277bd61fe2a2695f3b05822363483866e46bc9e73836f942fd18f349ff3420d7ad0d752d4c70735604bb1d0851 @@ -6,3 +6,4 @@ 768775 yrc_kill_yank jfw Implement the basic kill and yank commands. Also: rename prompt-backward command to prompt-back, to avert confusion with the anticipated prompt-back-word; update manual for the new commands plus clarifications & updated priorities. 768776 yrc_prompt_redraw_optimization jfw Optimize and clarify prompt drawing code; remove unneeded prompt_set_hscroll for consistency (this turned out to be needed after all; see an upcoming patch for fix and explanation). 768777 yrc_minor_command_reorder_2 jfw Reverse presentation order of back/forward prompt commands for consistency; also comment on possibly confusing differences between ctrl/meta functions. +768777 yrc_word_level_commands jfw Implement word-level motions and kills. A new global flag is needed for state tracking to coalesce multiple sequential kills into one. kill_start and kill_end are rebased on a common kill_range routine which also supports the new word-level variants. diff -uNr a/yrc/manual.txt b/yrc/manual.txt --- a/yrc/manual.txt 19368f1ab78bfbabd8a76ba4479d19b367086a5110f3f556f856f8da97f437d381a0036317f57c04cce4374ab45eefa8b6c41be2464af2372a422342437cf5f6 +++ b/yrc/manual.txt 9872a8ecfe57d6523b0c0a03a8c558f9c2f5b1a89360acd85bdda21c5f4f6f8ee828bba75714dd00c40bd68be859664c8d24762a8877dc58c64b37e1071e5443 @@ -259,6 +259,14 @@ Moves the cursor to the end of the line. + M-b, prompt-back-word + +Moves the cursor to the start of the current word, or preceeding word if not currently in one. + + M-f, prompt-forward-word + +Moves the cursor past the end of the current word, or following word if not currently in one. + C-p, Up: history-prev Recalls a previous line from input history. (History size is currently fixed at 1024 most recent entries.) @@ -283,6 +291,14 @@ Deletes text from cursor to end of line, saving it to the kill ring. + M-Backspace: kill-back-word + +Deletes text from cursor to start of current or preceeding word, saving it to the kill ring. Multiple kills in a row accumulate into a single entry, allowing the span to be pasted as a whole. + + M-d: kill-forward-word + +Deletes text from cursor to end of current or following word, saving it to the kill ring. + C-y: yank Inserts ("yanks" or "pastes") the most recent kill ring entry at cursor position. @@ -388,7 +404,6 @@ High-priority missing features: - - Word-level cursor motion and kill commands - Tab completion (nicks, slash commands) - Finish mode/op and close-net commands - Scrolling in window list if it exceeds available screen rows diff -uNr a/yrc/yrc.py b/yrc/yrc.py --- a/yrc/yrc.py 23e6700f25e00763e1c9b9db0d1586a617b3faf564ed83ba5450c58f5a6e3ad2bb8a2dbda6ad59f7ad959de983acb29e7f34294bad0a5615fc8011ed22749555 +++ b/yrc/yrc.py 017a6a4667f1a9b6a1a0b3a9ef272b70cef6c8943bf9cb674d849071bd7374a8c4ce98a5fc850106200971ae2ff3b60a32ce6e64fabe6cef0dc6e62f908ed3e1 @@ -92,6 +92,10 @@ END: 'prompt-end', LNX_END: 'prompt-end', + meta('b'): 'prompt-back-word', + + meta('f'): 'prompt-forward-word', + ctrl('P'): 'history-prev', UP: 'history-prev', @@ -108,6 +112,11 @@ ctrl('K'): 'kill-end', + meta(ctrl('H')): 'kill-back-word', + meta(ctrl('?')): 'kill-back-word', + + meta('d'): 'kill-forward-word', + ctrl('Y'): 'yank', meta('y'): 'yank-next', @@ -361,8 +370,8 @@ lines.append(line[start:]) return lines -def is_digit(c): - return '0' <= c <= '9' +is_digit = lambda c: '0' <= c <= '9' +is_alpha = lambda c: ('a' <= c <= 'z') or ('A' <= c <= 'Z') def parse_address(addr): addr = addr.rsplit(':', 1) @@ -380,6 +389,34 @@ format_address = lambda host_port: '%s:%d' % host_port +# Finding word boundaries: these currently use the simplistic emacs/readline style, e.g. punctuation isn't distinguished from whitespace and the spot you land on depends on the direction you're coming from. + +def rfind_word_start(chars, start_cursor): + # Always move at least 1 if possible + i = start_cursor - 1 + if i < 0: + return + # Continue until character at cursor is part of a word + while i > 0 and not is_alpha(chars[i]): + i -= 1 + # Continue until character before cursor is non-word + while i > 0 and is_alpha(chars[i-1]): + i -= 1 + return i + +def find_word_end(chars, start_cursor): + # Always move at least 1 if possible + i = start_cursor + 1 + if i > len(chars): + return + # Continue until character before cursor is part of a word + while i < len(chars) and not is_alpha(chars[i-1]): + i += 1 + # Continue until character at cursor is non-word + while i < len(chars) and is_alpha(chars[i]): + i += 1 + return i + int_of_bytes = lambda b: reduce(lambda acc, byte: acc*256 + ord(byte), b, 0) # int.bit_length() added in Python 2.7 @@ -570,7 +607,11 @@ quit_flag = _getf(6) schedule_quit = _setf(6) -# 7-8: available +extend_kill_flag = _getf(7) +set_extend_kill = _setf(7) +exit_extend_kill = _clrf(7) + +# 8: available buflist_flag = _getf(9) buflist_enter = _setf(9) @@ -890,6 +931,14 @@ 'scroll-bottom', )) +# Commands from the prompt keymap that don't reset the kill-extending state, in addition to YANK_SAFE_COMMANDS. (yank-next is accepted here too, because if we just killed then we didn't just yank, so it does nothing.) +KILL_COMMANDS = set(( + 'kill-start', + 'kill-end', + 'kill-back-word', + 'kill-forward-word', +)) + def prompt_insert(char): schedule_prompt_draw() c = prompt_cursor() @@ -927,6 +976,11 @@ def prompt_end(): prompt_set_cursor(len(prompt_chars())) +command('prompt-back-word')(lambda: + prompt_set_cursor(rfind_word_start(prompt_chars(), prompt_cursor()))) +command('prompt-forward-word')(lambda: + prompt_set_cursor(find_word_end(prompt_chars(), prompt_cursor()))) + @command('history-prev') def history_prev(): global history_pos, history_stash @@ -981,24 +1035,39 @@ chars.pop(c) schedule_prompt_draw() -@command('kill-start') -def kill_start(): - c = prompt_cursor() - if c > 0: # Don't send empty strings to the kill ring +def kill_range(a, b, is_forward): + if a < b: # Don't send empty strings to the kill ring chars = prompt_chars() - ring_append(kill_ring, ''.join(chars[:c])) - prompt_set_cursor(0) - del chars[:c] + s = ''.join(chars[a:b]) + # When killing multiple times in a row, accumulate into single entry. + if extend_kill_flag(): + acc = ring_get(kill_ring, -1) + if is_forward: + acc = acc + s + else: + acc = s + acc + ring_set(kill_ring, -1, acc) + else: + ring_append(kill_ring, s) + prompt_set_cursor(a) + del chars[a:b] schedule_prompt_draw() + set_extend_kill() -@command('kill-end') -def kill_end(): - c = prompt_cursor() - chars = prompt_chars() - if c < len(chars): # Don't send empty strings to the kill ring - ring_append(kill_ring, ''.join(chars[c:])) - del chars[c:] - schedule_prompt_draw() +command('kill-start')(lambda: kill_range(0, prompt_cursor(), False)) +command('kill-end')(lambda: kill_range( + prompt_cursor(), + len(prompt_chars()), + True)) + +command('kill-back-word')(lambda: kill_range( + rfind_word_start(prompt_chars(), prompt_cursor()), + prompt_cursor(), + False)) +command('kill-forward-word')(lambda: kill_range( + prompt_cursor(), + find_word_end(prompt_chars(), prompt_cursor()), + True)) @command('yank') def yank(): @@ -1275,15 +1344,18 @@ elif buflist_flag(): try_keymap(BUFLIST_KEYS) elif prompt_flag(): - # Prompt has the focus. A bit of an awkward special case: any editing action other than yank-next needs to reset yank state, if any. Rather than littering every command handler with exit_yank calls, we peek inside the box here at the common ancestor. + # Prompt has the focus. A bit of an awkward special case: any editing action other than yank-next needs to reset yank state, if any. Likewise, any editing action other than a kill needs to reset kill-extending state. Rather than littering every command handler with state reset calls (and risk missing some), we peek inside the box here at the common ancestor. cmd = PROMPT_KEYS.get(seq) if cmd is None: if len(seq) == 1 and not is_ctrl(seq): exit_yank() + exit_extend_kill() prompt_insert(seq) else: if cmd not in YANK_SAFE_COMMANDS: exit_yank() + if cmd not in KILL_COMMANDS: + exit_extend_kill() run_command(cmd) else: try_keymap(BUF_KEYS)