From 701600c678f2efcf007b85ec4d2f11f141dd73d3 Mon Sep 17 00:00:00 2001 From: syl20bnr Date: Sun, 5 Jan 2014 20:36:11 -0500 Subject: [PATCH] Add vi-mode from Ian Munsie --- .gitignore | 3 +- plugins/vi-mode/README.md | 62 +++++++ plugins/vi-mode/vi-mode-impl.py | 198 ++++++++++++++++++++ plugins/vi-mode/vi-mode.load | 306 +++++++++++++++++++++++++++++++ themes/simplevi/README.md | 12 ++ themes/simplevi/fish_prompt.fish | 4 + 6 files changed, 584 insertions(+), 1 deletion(-) create mode 100644 plugins/vi-mode/README.md create mode 100644 plugins/vi-mode/vi-mode-impl.py create mode 100644 plugins/vi-mode/vi-mode.load create mode 100644 themes/simplevi/README.md create mode 100644 themes/simplevi/fish_prompt.fish diff --git a/.gitignore b/.gitignore index 9de4023..0ec0f1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ custom/* -.DS_Store \ No newline at end of file +.DS_Store +*.pyc diff --git a/plugins/vi-mode/README.md b/plugins/vi-mode/README.md new file mode 100644 index 0000000..7450cb7 --- /dev/null +++ b/plugins/vi-mode/README.md @@ -0,0 +1,62 @@ +# vi-mode +Basic vi key bindings emulation for fish. + +## Install +Add `vi-mode` to your list of fish plugins in your `config.fish`. + +Example: + + set fish_plugins autojump vi-mode + +## Configuration +### Default mode +The default vi mode can be set by defining the variable `vi_mode_default` in +your `config.fish`. +If used, this variable must be set before loading `oh-my-fish`. +Its default value is `vi_mode_insert`. + +To set normal mode as default: + + set vi_mode_default vi_mode_normal + +### Mode indicator +The mode indicator is stored in the variable `vi_mode`. + +See the oh-my-fish theme `simplevi` for an example of integration of the +indicator into the prompt. + +### User key bindings +User additional key bindings can be defined with the function +`vi_mode_user_key_bindings`. If used, this function must be defined before +loading `oh-my-fish`. + +Example: + + function vi_mode_user_key_bindings + bind \el suppress-autosuggestion + end + +## Not implemented +- bind 2 vi-arg-digit +- bind y yank-direction +- bind g magic :-P +- bind ^a increment next number +- bind ^x decrement next number +- bind /?nN search (jk kind of does this) +- registers (maybe try to make sensible integration into X, like an + explicit yank with y goes to an X selection, while an implicit + delete with x etc. doesn't. "\* and "+ should natually go to the + appropriate X selection if possible) +- etc. + +## Author +This mode has been created by [Ian Munsie][ian_github], the original file can +be found [here][original]. +It has been adapted for oh-my-fish by [Sylvain Benner][syl_github]. + +[ian_github]: https://github.com/DarkStarSword +[original]: https://github.com/DarkStarSword/junk/blob/master/vi-mode.fish +[syl_github]: https://github.com/syl20bnr + + + diff --git a/plugins/vi-mode/vi-mode-impl.py b/plugins/vi-mode/vi-mode-impl.py new file mode 100644 index 0000000..ae22eb9 --- /dev/null +++ b/plugins/vi-mode/vi-mode-impl.py @@ -0,0 +1,198 @@ +# This script is part of the vi-mode plugin for fish +# author: Ian Munsie +# Adapted for oh-my-fish by Sylvain Benner + +import sys +from functools import reduce + +command = sys.argv[1] +direction = sys.argv[2] +new_pos = pos = int(sys.argv[3]) +lineno = int(sys.argv[4]) - 1 +cmdline_list = sys.argv[5:] +cmdline = '\n'.join(cmdline_list) + +def start(): + return (0, 0) +def end(): + return (len(cmdline), -1) +class not_found(Exception): pass + +def dir_0(): + return (reduce(lambda a,b: a + len(b) + 1, cmdline_list[:lineno], 0), 0) + +def dir_eol(): # end of line + before_len = reduce(lambda a,b: a + len(b) + 1, cmdline_list[:lineno], 0) + line_len = len(cmdline_list[lineno]) or 1 + return (before_len + line_len, -1) + +# These routines are all similar, they can probably be combined into one, but +# I'll make sure I get all working and understand the differences first +def dir_fnw(): # First Non-Whitespace + import re + len_before = reduce(lambda a,b: a + len(b) + 1, cmdline_list[:lineno], 0) + match = re.search('^\s*[^\s]', cmdline_list[lineno]) + if not match: + return start() + return (len_before + match.end()-1, 0) +dir__ = dir_fnw # XXX: I always used _ as this, but turns out that might not be quite right + +def _dir_w(regexp): + import re + + searchpart = cmdline[pos:] + match = re.search(regexp, searchpart) + if not match: + return end() + return (pos + match.end()-1, 0) + +def _dir_e(regexp): + import re + + searchpart = cmdline[pos+1:] + match = re.search(regexp, searchpart) + if not match: + return end() + return (pos+2 + match.start(), -1) + +def _dir_b(regexp): + import re + + if pos == 0: + return start() + + # Reverse the string instead of matching to right: + searchpart = cmdline[pos-1::-1] + match = re.search(regexp, searchpart) + if not match: + return start() + return (len(searchpart) - (match.start()+1), 0) + +def _dir_ge(regexp): + import re + + if pos == 0: + return start() + + # Reverse the string instead of matching to right: + searchpart = cmdline[pos::-1] + match = re.search(regexp, searchpart) + if not match: + return start() + return (len(searchpart) - (match.end()), 0) + +# Simple, but not inclusive enough: +# def dir_w(): return _dir_w(r'[^\w]\w') +# def dir_e(): return _dir_e(r'\w[^\w]') + +# Slightly too inclusive, e.g. fi--sh matches both '-' characters, but should only match one: +_dir_w_regexp = r'[^\w][^\s]|\w[^\w\s]' +_dir_e_regexp = r'[^\s][^\w]|[^\w\s]\w' +_dir_W_regexp = r'\s[^\s]' +_dir_E_regexp = r'[^\s]\s' + +def dir_w(): return _dir_w(_dir_w_regexp) +def dir_W(): return _dir_w(_dir_W_regexp) +def dir_e(): return _dir_e(_dir_e_regexp) +def dir_E(): return _dir_e(_dir_E_regexp) +def dir_b(): return _dir_b(_dir_e_regexp) +def dir_B(): return _dir_b(_dir_E_regexp) +def dir_ge(): return _dir_ge(_dir_w_regexp) +def dir_gE(): return _dir_ge(_dir_W_regexp) +def dir_cw(): return _dir_w(_dir_e_regexp) +def dir_cW(): return _dir_w(_dir_E_regexp) + +def dir_h(): + if pos: return (pos-1, 0) + return start() + +def dir_l(): + return (pos+1, 0) + +def dir_t(char): + new_pos = cmdline.find(char, pos+1) + if new_pos < 0: + raise not_found + return (new_pos, -1) + +def dir_T(char): + new_pos = cmdline.rfind(char, 0, pos) + if new_pos < 0: + raise not_found + return (new_pos+1, 0) + +def dir_f(char): return (dir_t(char)[0]+1, -1) +def dir_F(char): return (dir_T(char)[0]-1, 0) + +def cmd_delete(): + dst_pos = dir(direction) + if dst_pos >= pos: + new_cmdline = cmdline[:pos] + cmdline[dst_pos:] + return (new_cmdline, pos) + new_cmdline = cmdline[:dst_pos] + cmdline[pos:] + return (new_cmdline, dst_pos) + +def cmd_change(): + # 'Special case: 'cw' and 'cW' are treated like 'ce' and 'cE' if the cursor + # is on a non-blank. This is because 'cw' is interpreted as change-word, + # and a word does not include the following white space.' + # + # Note: Even with this special case the behaviour does not quite match what + # vim actually does in practice - try with the cursor on punctuation, or on + # the last character in a word. Specifically, the behaviour of cw differs + # from ce when the cursor is already on the last character of a 'word', for + # vim's definition of word. + # + # Because of this, we use a special direction to handle this case. + global direction + if direction in 'wW' and not cmdline[pos].isspace(): + direction = direction.replace('w', 'cw').replace('W', 'cW') + return cmd_delete() + +def cmd_o(): + above = '\n'.join(cmdline_list[:lineno + 1]) + below = '\n'.join(cmdline_list[lineno + 1:]) + return (above + '\n\n' + below, len(above)+1) + +def cmd_O(): + above = '\n'.join(cmdline_list[:lineno]) + below = '\n'.join(cmdline_list[lineno:]) + return (above + '\n\n' + below, len(above)+1) + +def _dir_cmd_func(func): + (pos1, pos2) = (pos, dir(direction)) + if pos2 < pos: + (pos1, pos2) = (pos2, pos1) + new_cmdline = cmdline[:pos1] + func(cmdline[pos1:pos2]) + cmdline[pos2:] + return (new_cmdline, pos1) + +# XXX: automagic completion sometimes hides the results of changing the case in these commands: +def cmd_upper(): return _dir_cmd_func(str.upper) +def cmd_lower(): return _dir_cmd_func(str.lower) +def cmd_swapcase(): return _dir_cmd_func(str.swapcase) + +def dir(d, cursor = False): + def validate(pos): + if pos < 0: return 0 + if pos > len(cmdline): return len(cmdline) + return pos + a = () + if ':' in d: + (d, a) = d.split(':', 1) + (new_pos, cursor_off) = globals()['dir_%s' % d](*a) + if cursor: + return validate(new_pos + cursor_off) + return validate(new_pos) + +def cmd(c): return globals()['cmd_%s' % c]() + +def cmd_normal(): + return (None, dir(direction, True)) + +try: + (cmdline, new_pos) = cmd(command) + if cmdline is not None: + print ( cmdline ) +except not_found: + new_pos = pos +print ( new_pos ) diff --git a/plugins/vi-mode/vi-mode.load b/plugins/vi-mode/vi-mode.load new file mode 100644 index 0000000..2996365 --- /dev/null +++ b/plugins/vi-mode/vi-mode.load @@ -0,0 +1,306 @@ +# vi:noexpandtab:sw=4:ts=4 +# +# vi-mode for fish +# author: Ian Munsie +# Adapted for oh-my-fish by Sylvain Benner +# +# Requirements: fish 2.0+ + +# I'm thinking about changing these to separate out the colours from the text. +# I'm undecided on this though - when I support count arguments there may be +# some benefit from displaying the count here, but that would also make the +# size of these strings vary, which I'd rather not do... +set -l cn (set_color normal 2>/dev/null) +set -g vi_mode_normal (set_color blue 2>/dev/null)'n'$cn +set -g vi_mode_replace (set_color red 2>/dev/null)'r'$cn +set -g vi_mode_REPLACE (set_color --background=red 2>/dev/null)'R'$cn +set -g vi_mode_insert (set_color green 2>/dev/null)'i'$cn +set -g vi_mode_delete (set_color red 2>/dev/null)'d'$cn +set -g vi_mode_change (set_color yellow 2>/dev/null)'c'$cn +set -g vi_mode_g (set_color blue 2>/dev/null)'g'$cn +set -g vi_mode_lower (set_color blue 2>/dev/null)'u'$cn +set -g vi_mode_upper (set_color blue 2>/dev/null)'U'$cn +set -g vi_mode_swapcase (set_color blue 2>/dev/null)'~'$cn + +set -g __vi_mode_undo_cmdline '' +set -g __vi_mode_undo_cmdline_pos 0 + +# default vi mode +if test -z $vi_mode_default + set -g vi_mode_default vi_mode_insert +end + +# additional user key bindings go in vi_mode_user_key_bindings +function __vi_mode_user_key_bindings + if functions | grep "vi_mode_user_key_bindings" > /dev/null + vi_mode_user_key_bindings + end +end + +function __vi_mode_direction_command + # commandline should always be last + set ret (python (dirname (status -f))/vi-mode-impl.py $argv[1] $argv[2] (commandline -C) (commandline -L) (commandline)) + set new_pos $ret[-1] + set -e ret[-1] # Guessing that deleting last element is likely to be faster than deleting first + if test (count $ret) -gt 0 + commandline -- $ret + end + commandline -C $new_pos +end + +function __vi_mode_common_emacs -d "common key bindings for all vi-like modes that are identical to the emacs mode" + # Feel free to expand this list for anything that makes sense, this is just + # a couple of hand-picked ones that seemed useful and/or I use :) + + bind \e\[A up-or-search + bind \e\[B down-or-search + bind -k down down-or-search + bind -k up up-or-search + + bind \e\[C forward-char + bind \e\[D backward-char + bind -k right forward-char + bind -k left backward-char + + bind \e\[H beginning-of-line + bind \e\[F end-of-line + # OS X SnowLeopard doesn't have these keys. Don't show an annoying error message. + bind -k home beginning-of-line 2> /dev/null + bind -k end end-of-line 2> /dev/null + + bind \cl 'clear; commandline -f repaint' + bind \cd delete-or-exit +end + +function __vi_mode_common -d "common key bindings for all vi-like modes" + __vi_mode_common_emacs + + bind \e vi_mode_normal + + # Can we put commandline into history when pressing ^C? + bind \cc '__vi_mode_save_cmdline; for i in (seq (count (commandline))); echo; end; commandline ""; vi_mode_insert' + + bind \n "commandline -f execute; "$vi_mode_default + __vi_mode_user_key_bindings +end + +function __vi_mode_common_insert -d "common key bindings for all insert vi-like modes" + __vi_mode_common + bind \e 'commandline -f backward-char; vi_mode_normal' + if functions -q vi_mode_user + vi_mode_user insert + end +end + +function __vi_mode_bind_directions + __vi_mode $argv[1] + + for direction in W w E e B b 0 _ h l + bind $direction "$argv[3]; __vi_mode_direction_command '$argv[1]' $direction; $argv[2]" + end + bind \$ "$argv[3]; __vi_mode_direction_command '$argv[1]' eol; $argv[2]" + bind \^ "$argv[3]; __vi_mode_direction_command '$argv[1]' fnw; $argv[2]" + for direction in f F t T + bind $direction "__vi_mode_bind_all '$argv[3]; __vi_mode_direction_command %q$argv[1]%q {$direction}:%k; $argv[2]'" + end + + bind g "__vi_mode_bind_directions_g $argv" +end + +function __vi_mode_bind_directions_g + for direction in e E + bind $direction "$argv[3]; __vi_mode_direction_command '$argv[1]' g$direction; $argv[2]" + end +end + +function __vi_mode_bind_all + # There seems to be some magic that doesn't work properly without this: + bind '' self-insert + + python -c " +command = '''$argv''' +for c in map(chr, range(0x20, 0x7f)): + q = '\"' # Enclose command in these + Q = '\'' # Other quote - for quotes inside command + if c == '\"': + l = r = r'\\%s' % c + (q, Q) = (Q, q) # Swap quotes + elif c in ['(', ')', '<', '>', ';', '|', '\'']: + l = r = r'\%s' % c + elif c == '\\\\': + l = r'\\\\' + r = r'\\\\\\\\' + elif c == '\$': + l = '\%s' % c + r = r\"'\%s'\" % c + else: + l = r = \"'%s'\" % c + print ( 'bind %s %s%s%s' % (l, q, command.replace('%k', r).replace('%q', Q), q)) + " | . +end + +function __vi_mode + # Is there a way to do this without eval? + # We really want something like a dictionary... + eval set -g vi_mode \$vi_mode_{$argv} + commandline -f repaint +end + +function __vi_mode_replace + __vi_mode replace + bind --erase --all + __vi_mode_common + + # backward-char should happen last, but only works if specified first + # (guess I should dig through the C code and figure out what is going + # on): + # __vi_mode_bind_all "commandline -f delete-char; commandline -i %k; commandline -f backward-char; vi_mode_normal" + __vi_mode_bind_all "__vi_mode_save_cmdline; commandline -f backward-char delete-char; commandline -i %k; vi_mode_normal" + + if functions -q vi_mode_user + vi_mode_user replace + end +end + +function __vi_mode_overwrite + __vi_mode REPLACE + bind --erase --all + __vi_mode_common_insert + __vi_mode_save_cmdline + + __vi_mode_bind_all "commandline -f delete-char; commandline -i %k" + if functions -q vi_mode_user + vi_mode_user overwrite + end +end + +function __vi_mode_save_cmdline + # Only vi style single level for now, patch to suppport vim style + # multi-level undo history welcome + set -g __vi_mode_undo_cmdline (commandline) + set -g __vi_mode_undo_cmdline_pos (commandline -C) +end + +function __vi_mode_undo + set -l cmdline (commandline) + set -l pos (commandline -C) + commandline $__vi_mode_undo_cmdline + commandline -C $__vi_mode_undo_cmdline_pos + set -g __vi_mode_undo_cmdline $cmdline + set -g __vi_mode_undo_cmdline_pos $pos +end + +function __vi_mode_g -d "vi-like key bindings for fish (commands starting with g)" + __vi_mode g + bind --erase --all + __vi_mode_bind_all 'vi_mode_normal' + __vi_mode_common + + bind I '__vi_mode_save_cmdline; commandline -f beginning-of-line; vi_mode_insert' + # XXX: automagic completion sometimes hides the results of changing the case in these commands: + bind u '__vi_mode_bind_directions lower vi_mode_normal __vi_mode_save_cmdline' + bind U '__vi_mode_bind_directions upper vi_mode_normal __vi_mode_save_cmdline' + bind \~ '__vi_mode_bind_directions swapcase vi_mode_normal __vi_mode_save_cmdline' + # TODO: The rest of the g commands + directions. + + __vi_mode_bind_directions_g normal vi_mode_normal '' + + if functions -q vi_mode_user + vi_mode_user g + end +end + +function vi_mode_normal -d "WIP vi-like key bindings for fish (normal mode)" + __vi_mode normal + + bind --erase --all + + # NOTE: bind '' self-insert seems to be required to allow the + # prompt to change, but we don't want unbound keys to be able to + # self-insert, so set the default binding, but bind everything to + # do nothing (which is wasteful, but seems to work): + __vi_mode_bind_all '' + + __vi_mode_common + + # Fish recently gained support for this, redirect to /dev/null so it + # doesn't fall over if running an old version of fish: + commandline -f suppress-autosuggestion 2>/dev/null + + bind i '__vi_mode_save_cmdline; vi_mode_insert' + bind I '__vi_mode_save_cmdline; __vi_mode_direction_command normal fnw; vi_mode_insert' + bind a '__vi_mode_save_cmdline; commandline -f forward-char; vi_mode_insert' + bind A '__vi_mode_save_cmdline; commandline -f end-of-line; vi_mode_insert' + bind o '__vi_mode_save_cmdline; __vi_mode_direction_command o ""; vi_mode_insert' + bind O '__vi_mode_save_cmdline; __vi_mode_direction_command O ""; vi_mode_insert' + + # Cool, these functions are pretty close to what I wanted: + # FIXME: Cursor not placed in correct position, but moving it prevents further searching + bind j down-or-search + bind k up-or-search + + bind x '__vi_mode_save_cmdline; commandline -f delete-char' + bind D '__vi_mode_save_cmdline; commandline -f kill-line' + # bind Y 'commandline -f kill-whole-line yank' + bind P '__vi_mode_save_cmdline; commandline -f yank' + bind p '__vi_mode_save_cmdline; commandline -f yank forward-char' # Yes, this is reversed. Otherwise it does the wrong thing. Go figure. + bind C '__vi_mode_save_cmdline; commandline -f kill-line; vi_mode_insert' + bind S '__vi_mode_save_cmdline; commandline -f kill-whole-line; vi_mode_insert' + bind s '__vi_mode_save_cmdline; commandline -f delete-char; vi_mode_insert' + bind r __vi_mode_replace + bind R __vi_mode_overwrite + + # XXX: The automagic completion sometimes displays the case from the + # command it wants to complete instead of the case actually on the + # commandline, so even though this works, it may not always appear to work. + # I'm not sure if I can do anything about that, I'll need to look at the + # code. Ideally I would turn off automagic completion whenever I'm not in + # insert mode. + bind \~ '__vi_mode_save_cmdline; commandline -f forward-char; __vi_mode_direction_command swapcase l' + + __vi_mode_bind_directions normal vi_mode_normal '' + bind d '__vi_mode_bind_directions delete vi_mode_normal __vi_mode_save_cmdline' + bind c '__vi_mode_bind_directions change vi_mode_insert __vi_mode_save_cmdline' + + # Override generic direction code for simple things that have a close + # match in fish's builtin commands, which should be faster: + bind h backward-char + bind l forward-char + bind 0 beginning-of-line + # bind \$ end-of-line #FIXME: Cursor position + # bind b backward-word # Note: built-in implementation only recently fixed. Also, before enabling this override, determine if this matches on the right characters + + bind g __vi_mode_g # MUST BE AFTER BIND_DIRECTIONS... I'm thinking about changing it so that this is all handled by bind_directions + bind u __vi_mode_undo + + # NOT IMPLEMENTED: + # bind 2 vi-arg-digit + # bind y yank-direction + # bind g magic :-P + # bind ^a increment next number + # bind ^x decrement next number + # bind /?nN search (jk kind of does this) + # registers (maybe try to make sensible integration into X, like an + # explicit yank with y goes to an X selection, while an implicit + # delete with x etc. doesn't. "* and "+ should natually go to the + # appropriate X selection if possible) + # etc. + + if functions -q vi_mode_user + vi_mode_user normal + end +end + +function vi_mode_insert -d "vi-like key bindings for fish (insert mode)" + __vi_mode insert + + fish_default_key_bindings + + __vi_mode_common_insert +end + +function fish_user_key_bindings + eval $vi_mode_default +end + +# vi:noexpandtab:sw=4:ts=4 diff --git a/themes/simplevi/README.md b/themes/simplevi/README.md new file mode 100644 index 0000000..6d64535 --- /dev/null +++ b/themes/simplevi/README.md @@ -0,0 +1,12 @@ +fish_prompt-simplevi +==================== + +The stock prompt with the vi indicator appended. +It is meant to be used with the `vi-mode` plugin. + +Screenshots: + +![normal mode](https://raw.github.com/syl20bnr/fish_prompt-simplevi/master/prompt_fish-simplevi-n.png) + +![insert mode](https://raw.github.com/syl20bnr/fish_prompt-simplevi/master/prompt_fish-simplevi-i.png) + diff --git a/themes/simplevi/fish_prompt.fish b/themes/simplevi/fish_prompt.fish new file mode 100644 index 0000000..536f659 --- /dev/null +++ b/themes/simplevi/fish_prompt.fish @@ -0,0 +1,4 @@ +# name: simplevi +function fish_prompt -d "Write out the prompt" + printf '%s@%s%s%s%s [%s]> ' (whoami) (hostname|cut -d . -f 1) (set_color $fish_color_cwd) (prompt_pwd) (set_color normal) $vi_mode +end