diff options
| -rw-r--r-- | docs/General/Signals.pod | 6 | ||||
| -rw-r--r-- | docs/Irssi/TextUI/StatusbarItem.pod | 17 | ||||
| -rw-r--r-- | feature-tests/internal_signals.pl | 14 | ||||
| -rw-r--r-- | feature-tests/sbar_test.pl | 44 | ||||
| -rw-r--r-- | feature-tests/text_intercept.pl | 33 | ||||
| -rw-r--r-- | prompt_info/overlays.pl | 89 | ||||
| -rw-r--r-- | prompt_info/prompt_info.pl | 26 | ||||
| -rw-r--r-- | prompt_info/prompt_replace.pl | 2 | ||||
| -rw-r--r-- | vim-mode/vim_mode.pl | 2098 | 
9 files changed, 1848 insertions, 481 deletions
| diff --git a/docs/General/Signals.pod b/docs/General/Signals.pod index 4720bee..2c83262 100644 --- a/docs/General/Signals.pod +++ b/docs/General/Signals.pod @@ -1606,6 +1606,12 @@ B<Requires to work properly:>  =back +This signal is called multiple times for a given print operation, in a fashion +similar to run-length coding.  A single line of printed output which varies in +colour may emit this signal multiple times, once for each colour change.  The +C<$fg>, C<$bg>, and C<$flags> contain the formatting information for C<$text>. + +  =item C<"gui print text finished">  =over diff --git a/docs/Irssi/TextUI/StatusbarItem.pod b/docs/Irssi/TextUI/StatusbarItem.pod index 87de288..b62890c 100644 --- a/docs/Irssi/TextUI/StatusbarItem.pod +++ b/docs/Irssi/TextUI/StatusbarItem.pod @@ -4,11 +4,26 @@ __END__  Irssi::TextUI::StatusbarItem +=head1 DESCRIPTION +  =head1 FIELDS  =head1 METHODS  =head2 C<default_handler> -I<undocumented> +=over + +=item C<$item> + +=item C<$get_size_only> + +=item C<$str> + +=item C<$data> + +=item C<$escape_vars> - defaults to C<TRUE> + +=back + diff --git a/feature-tests/internal_signals.pl b/feature-tests/internal_signals.pl new file mode 100644 index 0000000..e40587f --- /dev/null +++ b/feature-tests/internal_signals.pl @@ -0,0 +1,14 @@ +# not a complete script, just a useful snippet +Irssi::signal_register({'complete command set' +                        => ["glistptr_char*", "Irssi::UI::Window", +                                "string", "string", "intptr"]}); + +my @res = (); +my $num; +Irssi::signal_emit('complete command set', \@res, Irssi::active_win(), +                   '', '', \$num); + +print "results: @res"; + +# will return all the possible completions for the /set command.  you can filter +# it by changing the 2 empty strings (word-fragment, and line context) diff --git a/feature-tests/sbar_test.pl b/feature-tests/sbar_test.pl index 6523842..121bc52 100644 --- a/feature-tests/sbar_test.pl +++ b/feature-tests/sbar_test.pl @@ -29,3 +29,47 @@ sub foo_sb {  }  Irssi::statusbar_item_register ('foo_bar', 0, 'foo_sb'); + +__END__ +# Name                           Type   Placement Position Visible +# window                         window bottom    1        active +# window_inact                   window bottom    1        inactive +# prompt                         root   bottom    0        always +# topic                          root   top       1        always + +# Statusbar: prompt +# Type     : root +# Placement: bottom +# Position : 0 +# Visible  : always +# Items    : Name                                Priority  Alignment +#          : prompt                              0         left +#          : prompt_empty                        0         left +#          : input                               10        left +# +# STATUSBAR <name> ENABLE +# STATUSBAR <name> DISABLE +# STATUSBAR <name> RESET +# STATUSBAR <name> TYPE window|root +# STATUSBAR <name> PLACEMENT top|bottom +# STATUSBAR <name> POSITION <num> +# STATUSBAR <name> VISIBLE always|active|inactive +# STATUSBAR <name> ADD +#                  [-before | -after <item>] [-priority #] +#                  [-alignment left|right] <item> +# +# STATUSBAR <name> REMOVE <item> +# +# Commands for modifying the statusbar. +# +# /STATUSBAR +#    - Display all statusbars. +# +# /STATUSBAR <name> +#    - display elements of statusbar <name> +# +# Irssi commands: +# statusbar add     statusbar enable    statusbar position  statusbar reset +# statusbar visible statusbar disable   statusbar placement statusbar remove +# statusbar type + diff --git a/feature-tests/text_intercept.pl b/feature-tests/text_intercept.pl new file mode 100644 index 0000000..932703e --- /dev/null +++ b/feature-tests/text_intercept.pl @@ -0,0 +1,33 @@ +use strict; +use Irssi; +use Irssi::TextUI; # for sbar_items_redraw + +use vars qw($VERSION %IRSSI); +$VERSION = "1.0.1"; +%IRSSI = ( +	authors         => "shabble", +	contact         => 'shabble+irssi@metavore.org, shabble@#irssi/Freenode', +	name            => "", +	description     => "", +	license         => "Public Domain", +	changed         => "" +); + +my $ignore_flag = 0; + +Irssi::signal_add 'print text' => \&handle_text; + + +sub handle_text { +    my ($dest, $text, $stripped) = @_; + +    return if $ignore_flag; + +    Irssi::signal_stop(); + +    $text =~ s/a/b/g; + +    $ignore_flag = 1; +    $dest->print($text); +    $ignore_flag = 0; +} diff --git a/prompt_info/overlays.pl b/prompt_info/overlays.pl new file mode 100644 index 0000000..47a3eef --- /dev/null +++ b/prompt_info/overlays.pl @@ -0,0 +1,89 @@ +# temp place for dumping all the stuff that doesn't belong in uberprompt. + +# overlay  := { $num1 => line1, $num2 => line2 } +# line     := [ region, region, region ] +# region   := { start => x, end => y, ...? } + +my $overlay; + + + +sub _add_overlay_region { +    my ($line, $start, $end, $text, $len) = @_; +    my $region = { start => $start, +                   end => $end, +                   text => $text, +                   len => $len }; + +    my $o_line = $overlay->{$line}; + +    unless (defined $o_line) { +        $o_line = []; +        $overlay->{$line} = $o_line; +    } + +    foreach my $cur_region (@$o_line) { +        if (_region_overlaps($cur_region, $region)) { +            # do something. +            print "Region overlaps"; +            last; +        } +    } + +    push @$o_line, $region; + +} + +sub _remove_overlay_region { +    my ($line, $start, $end) = @_; + +    my $o_line = $overlay->{$line}; +    return unless $o_line; + +    my $i = 0; +    foreach my $region (@$o_line) { +        if ($region->{start} == $start && $region->{end} == $end) { +            last; +        } +        $i++; +    } +    splice @$o_line, $i, 1, (); # remove it. +} + +sub _redraw_overlay { +    foreach my $line_num (sort keys %$overlay) { +        my $line = $overlay->{$line_num}; + +        foreach my $region (@$line) { +            Irssi::gui_printtext($region->{start}, $line_num, +                                 $region->{text}); +        } +    } +} + +sub init { + +} +sub _clear_overlay { +    Irssi::active_win->view->redraw(); +} + +sub _draw_overlay_menu { + +    my $w = 10; + +    my @lines = ( +                 '%7+' . ('-' x $w) . '+%n', +                 sprintf('%%7|%%n%*s%%7|%%n', $w, 'bacon'), +                 sprintf('|%*s|', $w, 'bacon'), +                 sprintf('|%*s|', $w, 'bacon'), +                 sprintf('|%*s|', $w, 'bacon'), +                 sprintf('|%*s|', $w, 'bacon'), +                 sprintf('|%*s|', $w, 'bacon'), +                 '%7+' . ('-' x $w) . '+%n', +                ); +    my $i = 10; # start vert offset. +    for my $line (@lines) { +        Irssi::gui_printtext(int ($term_w / 2), $i++, $line); +    } +} diff --git a/prompt_info/prompt_info.pl b/prompt_info/prompt_info.pl index 51e5c16..8ad63ba 100644 --- a/prompt_info/prompt_info.pl +++ b/prompt_info/prompt_info.pl @@ -9,10 +9,30 @@  #  #    prompt = "[$*$prompt_additional] "  # -# Then add this script to your autorun directory (~/.irssi/scripts/autorun/) +# Then place this script to your ~/.irssi/scripts directory (~/.irssi/scripts/) +# and symlink it to the ~/.irssi/scripts/autorun directory (which may need to +# be created first)  # -# You can modify your prompt content by using the '/set_prompt <string>' command, -# or from scripts by Irssi:signal_emit('change prompt', $string); +# You can also load it manually once the theme has been edited via +# +# /script load prompt_info.pl +# +# You will also need to reload your theme with the following command: +# +# /script exec Irssi::themes_reload() +# +# Once loaded, you can modify your prompt content by using the following command: +# +# /set_prompt <string> +# +# You can also use it from other scripts by issuing a signal as follows: +# +# Irssi:signal_emit('change prompt', +# +# report bugs / feature requests to http://github.com/shabble/irssi-scripts/issues +# +# NOTE: it does not appear to be possible to use colours in your prompt at present. +# This is unlikely to change without source-code changes to Irssi itself.  use strict;  use warnings; diff --git a/prompt_info/prompt_replace.pl b/prompt_info/prompt_replace.pl index 4cab4be..30120f7 100644 --- a/prompt_info/prompt_replace.pl +++ b/prompt_info/prompt_replace.pl @@ -130,7 +130,6 @@ sub init {      Irssi::signal_add('change prompt' => \&change_prompt_sig);      Irssi::signal_register({'prompt changed' => [qw/string int/]}); -  }  sub change_prompt_sig { @@ -195,6 +194,7 @@ sub uberprompt_draw {      my $ret = $sb_item->default_handler($get_size_only, $p_copy, '', 0);      Irssi::signal_emit('prompt changed', $p_copy, $sb_item->{size}); +      return $ret;  } diff --git a/vim-mode/vim_mode.pl b/vim-mode/vim_mode.pl index 0aa6b61..134874d 100644 --- a/vim-mode/vim_mode.pl +++ b/vim-mode/vim_mode.pl @@ -1,45 +1,160 @@  # A script to emulate some of the vi(m) features for the Irssi inputline.  # -# Currently supported features: +# It should work fine with at least 0.8.12 and later versions. However some +# features are disabled in older versions (see below for details). Perl >= +# 5.8.1 is recommended for UTF-8 support (which can be disabled if necessary). +# Please report bugs in older versions as well, we'll try to fix them. +# +# Any behavior different from Vim (unless explicitly documented) should be +# considered a bug and reported. +# +# NOTE: This script is still under heavy development, and there may be bugs. +# Please submit reproducible sequences to the bug-tracker at: +# http://github.com/shabble/irssi-scripts/issues +# +# or contact rudi_s or shabble on irc.freenode.net (#irssi and #irssi_vim) +# +# +# Features: +# +# It supports most commonly used command mode features: +# +# * Insert/Command mode. Escape and Ctrl-C enter command mode. +#   /set vim_mode_cmd_seq j allows to use jj as Escape (any other character +#   can be used as well). +# * Cursor motion: h l 0 ^ $ <space> f t F T +# * History motion: j k gg G +#   gg moves to the oldest (first) history line. +#   G without a count moves to the current input line, with a count it goes to +#   the count-th history line (1 is the oldest). +# * Cursor word motion: w b ge e W gE B E +# * Word objects (only the following work yet): aw aW +# * Yank and paste: y p P +# * Change and delete: c d +# * Delete at cursor: x X +# * Replace at cursor: r +# * Insert mode: i a I A +# * Switch case: ~ +# * Repeat change: . +# * Repeat ftFT: ; , +# * Registers: "a-"z "" "* "+ "_ (black hole) +#   Appending to register with "A-"Z +#   "" is the default yank/delete register. +#   The special registers "* "+ contain both irssi's cut-buffer. +# * Line-wise shortcuts: dd cc yy +# * Shortcuts: s S C D +# * Scroll the scrollback buffer: Ctrl-D Ctrl-U Ctrl-F Ctrl-B +# * Switch to last active window: Ctrl-6/Ctrl-^ +# * Switch split windows: Ctrl-W j Ctrl-W k +# * Undo/Redo: u Ctrl-R +# +# Counts and combinations work as well, e.g. d5fx or 3iabc<esc> +# Repeat also supports counts. +# +# The following insert mode mappings are supported: +# +# * Insert register content: Ctrl-R x (where x is the register to insert) +# +# Ex-mode supports (activated by : in command mode) the following commands: +# +# * Switching buffers: :b <num> - switch to channel number +#                      :b#      - switch to last channel +#                      :b <partial-channel-name> +#                      :b <partial-server>/<partial-channel> +#                      :buffer {args} (same as :b) +#                      :bn[ext] - switch to next window +#                      :bp[rev] - switch to previous window +# * Close window:      :bd[elete] +# * Display windows:   :ls :buffers +# * Display registers: :reg[isters] :di[splay] {args} +# * Display undolist:  :undol[ist] (mostly used for debugging) +# +# The following irssi settings are available: +# +# * vim_mode_utf8: support UTF-8 characters, default on +# * vim_mode_debug: enable debug output, default off +# * vim_mode_cmd_seq: char that when double-pressed simulates <esc> +# +# The following statusbar items are available: +# +# * vim_mode: displays current mode +# * vim_windows: displays windows selected with :b  # -# * Insert/Command mode. Escape enters command mode. -# * cursor motion with: h l 0 ^ $ -# * history motion with j k -# * cursor word motion with: w b e W B E -# * change/delete: c d C D -# * delete at cursor: x -# * replace at cursor: r -# * Insert mode at pos: i a -# * Insert mode at start: I -# * insert mode at end: A -# * yank and paste: y p P -# * switch case: ~ -# * repeat change: . -# * change/change/yank line: cc dd yy S -# * Combinations like in Vi, e.g. d5fx -# * goto window: 5G  # -# TODO: -# * /,?,n to search through history (like history_search.pl) -# * u = undo (how many levels, branching?!) redo? -# * use irssi settings for some of the features (esp. debug) - -# Known bugs: -# * count with insert mode: 3iabc<esc> doesn't work -# * repeat insert mode: iabc<esc>. only enters insert mode -  # Installation:  # -# The usual, stick in scripts dir, /script load vim_mode.pl ... +# As always copy the script into .irssi/scripts and load it with +#     /script load # vim_mode.pl +# +# Use the following command to get a statusbar item that shows which mode +# you're in. +# +#     /statusbar window add vim_mode +# +# And the following to let :b name display a list of matching channels +# +#     /statusbar window add vim_windows +# +# +# Dependencies: +# +# For proper :ex mode support, requires the installation of prompt_info.pl +#  http://github.com/shabble/irssi-scripts/raw/master/prompt_info/prompt_info.pl +# +# and follow the instructions in the top of that file for installation +# instructions. +# +# If you don't need Ex-mode, you can run vim_mode.pl without the +# prompt_info.pl script. +# +# +# Irssi requirements: +# +# 0.8.12 and above should work fine. However the following features are +# disabled in irssi < 0.8.13: +# +# * j k (only with count, they work fine without count in older versions) +# * gg G +# +# +# Known bugs: +# +# * count before register doesn't work: e.g. 3"ap doesn't work, but "a3p does +# +# +# TODO: +# * History: +#   * /,?,n,N to search through history (like history_search.pl) +# * Window switching (:b) +#  * Tab completion of window(-item) names +#  * non-sequential matches(?) +# +# WONTFIX - things we're not ever likely to do +# * Macros +# +# LICENCE: +# +# Copyright (c) 2010 Tom Feist & Simon Ruderich +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +#  # -# Use the following command to get a statusbar item that shows which mode you're -# in. Annoying vi bleeping not yet supported :) - -# /statusbar window add vim_mode to get the status. - -# NOTE: This is still under extreme development, and there's a whole bunch of -# debugging output. Edit the DEBUG constant to remove it if it bothers you. -  # Have fun!  use strict; @@ -50,6 +165,7 @@ use List::Util;  use Irssi;  use Irssi::TextUI;              # for sbar_items_redraw +use Irssi::Irc;                 # necessary for 0.8.14  use vars qw($VERSION %IRSSI); @@ -62,22 +178,170 @@ $VERSION = "1.0.1";                      . 'rudi_s@#irssi/Freenode',     name            => "vim_mode",     description     => "Give Irssi Vim-like commands for editing the inputline", -   license         => "Public Domain", -   changed         => "20/9/2010" +   license         => "MIT", +   changed         => "28/9/2010"    );  # CONSTANTS -  sub M_CMD() { 1 } # command mode  sub M_INS() { 0 } # insert mode  sub M_EX () { 2 } # extended mode (after a :?) -# word and non-word regex +# operator command +sub C_OPERATOR () { 0 } +# normal commmand +sub C_NORMAL () { 1 } +# command taking another key as argument +sub C_NEEDSKEY () { 2 } +# text-object commmand (i a) +sub C_TEXTOBJECT () { 3 } +# commands entering insert mode +sub C_INSERT () { 4 } +# ex-mode commands +sub C_EX () { 5 } + +# word and non-word regex, keep in sync with setup_changed()!  my $word     = qr/[\w_]/o;  my $non_word = qr/[^\w_\s]/o; +# COMMANDS + +# All available commands in command mode, they are stored with a char as key, +# but this is not necessarily the key the command is currently mapped to. +my $commands +  = { +     # operators +     c => { char => 'c', func => \&cmd_operator_c, type => C_OPERATOR, +            repeatable => 1 }, +     d => { char => 'd', func => \&cmd_operator_d, type => C_OPERATOR, +            repeatable => 1 }, +     y => { char => 'y', func => \&cmd_operator_y, type => C_OPERATOR, +            repeatable => 1 }, + +     # arrow like movement +      h  => { char => 'h', func => \&cmd_h, type => C_NORMAL }, +      l  => { char => 'l', func => \&cmd_l, type => C_NORMAL }, +     ' ' => { char => '<space>', func => \&cmd_space, type => C_NORMAL }, +     # history movement +     j  => { char => 'j',  func => \&cmd_j,  type => C_NORMAL }, +     k  => { char => 'k',  func => \&cmd_k,  type => C_NORMAL }, +     gg => { char => 'gg', func => \&cmd_gg, type => C_NORMAL }, +     G  => { char => 'G',  func => \&cmd_G,  type => C_NORMAL, +             needs_count => 1 }, +     # char movement, take an additional parameter and use $movement +      f  => { char => 'f', func => \&cmd_f, type => C_NEEDSKEY }, +      t  => { char => 't', func => \&cmd_t, type => C_NEEDSKEY }, +      F  => { char => 'F', func => \&cmd_F, type => C_NEEDSKEY }, +      T  => { char => 'T', func => \&cmd_T, type => C_NEEDSKEY }, +     ';' => { char => ';', func => \&cmd_semicolon, type => C_NORMAL }, +     ',' => { char => ',', func => \&cmd_comma, type => C_NORMAL }, +     # word movement +     w  => { char => 'w',  func => \&cmd_w,  type => C_NORMAL }, +     b  => { char => 'b',  func => \&cmd_b,  type => C_NORMAL }, +     e  => { char => 'e',  func => \&cmd_e,  type => C_NORMAL }, +     ge => { char => 'ge', func => \&cmd_ge, type => C_NORMAL }, +     W  => { char => 'W',  func => \&cmd_W,  type => C_NORMAL }, +     B  => { char => 'B',  func => \&cmd_B,  type => C_NORMAL }, +     E  => { char => 'E',  func => \&cmd_E,  type => C_NORMAL }, +     gE => { char => 'gE', func => \&cmd_gE, type => C_NORMAL }, +     # text-objects, leading _ means can't be mapped! +     _i => { char => '_i', func => \&cmd__i, type => C_TEXTOBJECT }, +     _a => { char => '_a', func => \&cmd__a, type => C_TEXTOBJECT }, +     # line movement +     '0' => { char => '0', func => \&cmd_0, type => C_NORMAL }, +     '^' => { char => '^', func => \&cmd_caret, type => C_NORMAL }, +     '$' => { char => '$', func => \&cmd_dollar, type => C_NORMAL }, +     # delete chars +     x => { char => 'x', func => \&cmd_x, type => C_NORMAL, +            repeatable => 1 }, +     X => { char => 'X', func => \&cmd_X, type => C_NORMAL, +            repeatable => 1 }, +     s => { char => 's', func => \&cmd_s, type => C_NORMAL, +            repeatable => 1 }, # operator c takes care of insert mode +     S => { char => 'S', func => \&cmd_S, type => C_NORMAL, +            repeatable => 1 }, # operator c takes care of insert mode +     # insert mode +     i => { char => 'i', func => \&cmd_i, type => C_INSERT, +            repeatable => 1 }, +     I => { char => 'I', func => \&cmd_I, type => C_INSERT, +            repeatable => 1 }, +     a => { char => 'a', func => \&cmd_a, type => C_INSERT, +            repeatable => 1 }, +     A => { char => 'A', func => \&cmd_A, type => C_INSERT, +            repeatable => 1 }, +     # replace +     r => { char => 'r', func => \&cmd_r, type => C_NEEDSKEY, +              repeatable => 1 }, +     # paste +     p => { char => 'p', func => \&cmd_p, type => C_NORMAL, +            repeatable => 1 }, +     P => { char => 'P', func => \&cmd_P, type => C_NORMAL, +            repeatable => 1 }, +     # to end of line +     C => { char => 'C', func => \&cmd_C, type => C_NORMAL, +            repeatable => 1 }, +     D => { char => 'D', func => \&cmd_D, type => C_NORMAL, +            repeatable => 1 }, +     # scrolling +     "\x04" => { char => '<c-d>', func => \&cmd_ctrl_d, type => C_NORMAL, +                 repeatable => 1 }, # half screen down +     "\x15" => { char => '<c-u>', func => \&cmd_ctrl_u, type => C_NORMAL, +                 repeatable => 1 }, # half screen up +     "\x06" => { char => '<c-f>', func => \&cmd_ctrl_f, type => C_NORMAL, +                 repeatable => 1 }, # screen down +     "\x02" => { char => '<c-b>', func => \&cmd_ctrl_b, type => C_NORMAL, +                 repeatable => 1 }, # screen up +     # window switching +     "\x17j" => { char => '<c-w>j', func => \&cmd_ctrl_wj, type => C_NORMAL }, +     "\x17k" => { char => '<c-w>k', func => \&cmd_ctrl_wk, type => C_NORMAL }, +     "\x1e"  => { char => '<c-6>',  func => \&cmd_ctrl_6,  type => C_NORMAL }, +     # misc +     '~'  => { char => '~', func => \&cmd_tilde, type => C_NORMAL, +               repeatable => 1 }, +     '"'  => { char => '"', func => \&cmd_register, type => C_NEEDSKEY }, +     '.'  => { char => '.', type => C_NORMAL, repeatable => 1 }, +     ':'  => { char => ':', type => C_NORMAL }, +     "\n" => { char => '<cr>', type => C_NORMAL }, # return +     # undo +     'u'    => { char => 'u',     func => \&cmd_undo, type => C_NORMAL }, +     "\x12" => { char => '<c-r>', func => \&cmd_redo, type => C_NORMAL }, +    }; + +# All available commands in Ex-Mode. +my $commands_ex +  = { +     s         => { func => \&ex_substitute => type => C_EX }, +     bnext     => { func => \&ex_bnext      => type => C_EX }, +     bn        => { func => \&ex_bnext      => type => C_EX }, +     bprev     => { func => \&ex_bprev      => type => C_EX }, +     bp        => { func => \&ex_bprev      => type => C_EX }, +     bdelete   => { func => \&ex_bdelete    => type => C_EX }, +     bd        => { func => \&ex_bdelete    => type => C_EX }, +     buffer    => { func => \&ex_buffer     => type => C_EX }, +     b         => { func => \&ex_buffer     => type => C_EX }, +     registers => { func => \&ex_registers  => type => C_EX }, +     reg       => { func => \&ex_registers  => type => C_EX }, +     display   => { func => \&ex_registers  => type => C_EX }, +     di        => { func => \&ex_registers  => type => C_EX }, +     buffers   => { func => \&ex_buffers    => type => C_EX }, +     ls        => { func => \&ex_buffers    => type => C_EX }, +     undolist  => { func => \&ex_undolist   => type => C_EX }, +     undol     => { func => \&ex_undolist   => type => C_EX }, +    }; + +# MAPPINGS + +# default command mode mappings +my $maps = {}; + +# Add all default mappings. +foreach my $char (keys %$commands) { +    next if $char =~ /^_/; # skip private commands (text-objects for now) +    add_map($char, $commands->{$char}); +} +  # GLOBAL VARIABLES  my $DEBUG_ENABLED = 0; @@ -93,26 +357,39 @@ my @input_buf;  my $input_buf_timer;  my $input_buf_enabled = 0; +# insert mode repeat buffer, used to repeat (.) last insert +my @insert_buf; +  # flag to allow us to emulate keystrokes without re-intercepting them  my $should_ignore = 0;  # ex mode buffer  my @ex_buf; +# we are waiting for another mapped key (e.g. g pressed, but there are +# multiple mappings like gg gE etc.) +my $pending_map = undef; +  # for commands like 10x  my $numeric_prefix = undef; -# vi operators like d, c, .. +# current operator as $command hash  my $operator = undef;  # vi movements, only used when a movement needs more than one key (like f t).  my $movement = undef;  # last vi command, used by .  my $last    = { -     'char' => undef, +     'cmd' => $commands->{i}, # = i to support . when loading the script       'numeric_prefix' => undef,       'operator' => undef,       'movement' => undef, -     'register' => undef, +     'register' => '"', +    }; +# last ftFT movement, used by ; and , +my $last_ftFT +  = { +     type => undef, # f, t, F or T +     char => undef,      };  # what Vi mode we're in. We start in insert mode. @@ -121,17 +398,27 @@ my $mode = M_INS;  # current active register  my $register = '"'; -# vi registers, " is the default register +# vi registers  my $registers    = { -     '"' => '' +     '"' => '', # default register +     '+' => '', # contains irssi's cut buffer +     '*' => '', # same +     '_' => '', # black hole register, always empty      }; +foreach my $char ('a' .. 'z') { +    $registers->{$char} = ''; +}  # current imap still pending (first character entered)  my $imap = undef;  # maps for insert mode -my $imaps = {}; +my $imaps +  = { +     # ctrl-r, insert register +     "\x12" => { map  => undef, func => \&insert_ctrl_r }, +    };  # index into the history list (for j,k)  my $history_index = undef; @@ -157,130 +444,95 @@ sub script_is_loaded {  vim_mode_init(); -# vi-operators like d, c; they don't move the cursor -my $operators -  = { -     'c' => { func => \&cmd_operator_c }, -     'd' => { func => \&cmd_operator_d }, -     'y' => { func => \&cmd_operator_y }, -    }; - -# vi-moves like w,b; they move the cursor and may get combined with an -# operator; also things like i/I are listed here, not entirely correct but -# they work in a similar way -my $movements -  = { -     # arrow like movement -     'h' => { func => \&cmd_movement_h }, -     'l' => { func => \&cmd_movement_l }, -     ' ' => { func => \&cmd_movement_space }, -     'j' => { func => \&cmd_movement_j }, -     'k' => { func => \&cmd_movement_k }, -     # char movement, take an additional parameter and use $movement -     'f' => { func => \&cmd_movement_f }, -     't' => { func => \&cmd_movement_t }, -     'F' => { func => \&cmd_movement_F }, -     'T' => { func => \&cmd_movement_T }, -     # word movement -     'w' => { func => \&cmd_movement_w }, -     'b' => { func => \&cmd_movement_b }, -     'e' => { func => \&cmd_movement_e }, -     'W' => { func => \&cmd_movement_W }, -     'B' => { func => \&cmd_movement_B }, -     'E' => { func => \&cmd_movement_E }, -     # line movement -     '0' => { func => \&cmd_movement_0 }, -     '^' => { func => \&cmd_movement_caret }, -     '$' => { func => \&cmd_movement_dollar }, -     # delete chars -     'x' => { func => \&cmd_movement_x }, -     'X' => { func => \&cmd_movement_X }, -     # insert mode -     'i' => { func => \&cmd_movement_i }, -     'I' => { func => \&cmd_movement_I }, -     'a' => { func => \&cmd_movement_a }, -     'A' => { func => \&cmd_movement_A }, -     # replace mode -     'r' => { func => \&cmd_movement_r }, -     # paste -     'p' => { func => \&cmd_movement_p }, -     'P' => { func => \&cmd_movement_P }, -     # to end of line -     'C' => { func => \&cmd_movement_dollar }, -     'D' => { func => \&cmd_movement_dollar }, -     # change window -     'G' => { func => \&cmd_movement_G }, -     # misc -     '~' => { func => \&cmd_movement_tilde }, -     '.' => {}, -     '"' => { func => \&cmd_movement_register }, -     # undo -     'u'    => { func => \&cmd_undo }, -     "\x12" => { func => \&cmd_redo }, +# INSERT MODE COMMANDS -    }; - -# special movements which take an additional key -my $movements_multiple = -    { -     'f' => undef, -     't' => undef, -     'F' => undef, -     'T' => undef, -     'r' => undef, -     '"' => undef, -    }; - - -sub cmd_undo { -    print "Undo!" if DEBUG; -    if ($undo_index > $#undo_buffer) { -        $undo_index = $#undo_buffer; -        print "No further undo." if DEBUG; -    } elsif ($undo_index != $#undo_buffer) { -        $undo_index++; -    } +sub insert_ctrl_r { +    my ($key) = @_; -    print "Undoing entry $undo_index of " . $#undo_buffer if DEBUG; +    my $char = chr($key); +    return if not defined $registers->{$char} or not $registers->{$char}; -    _restore_undo_entry($undo_index); +    my $pos = _insert_at_position($registers->{$char}, 1, _input_pos()); +    _input_pos($pos + 1);  } -sub cmd_redo { -    print "Redo!" if DEBUG; -} + +# COMMAND MODE OPERATORS  sub cmd_operator_c { -    my ($old_pos, $new_pos, $move) = @_; +    my ($old_pos, $new_pos, $move, $repeat) = @_; + +    # Changing a word or WORD doesn't delete the last space before a word, but +    # not if we are on that whitespace before the word. +    if ($move eq 'w' or $move eq 'W') { +        my $input = _input(); +        if ($new_pos - $old_pos > 1 and +                substr($input, $new_pos - 1, 1) =~ /\s/) { +            $new_pos--; +        } +    } -    cmd_operator_d($old_pos, $new_pos, $move); -    _update_mode(M_INS); +    cmd_operator_d($old_pos, $new_pos, $move, $repeat, 1); + +    if (!$repeat) { +        _update_mode(M_INS); +    } else { +        my $pos = _input_pos(); +        $pos = _insert_buffer(1, $pos); +        _input_pos($pos); +    }  }  sub cmd_operator_d { -    my ($old_pos, $new_pos, $move) = @_; +    my ($old_pos, $new_pos, $move, $repeat, $change) = @_;      my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move);      # Remove the selected string from the input.      my $input = _input(); -    $registers->{$register} = substr $input, $pos, $length, ''; +    my $string = substr $input, $pos, $length, ''; +    if ($register =~ /[A-Z]/) { +        $registers->{lc $register} .= $string; +        print "Deleted into $register: ", $registers->{lc $register} if DEBUG; +    } else { +        $registers->{$register} = $string; +        print "Deleted into $register: ", $registers->{$register} if DEBUG; +    }      _input($input); -    print "Deleted into $register: " . $registers->{$register} if DEBUG; -    # Move the cursor at the right position. +    # Prevent moving after the text when we delete the last character. But not +    # when changing (C). +    $pos-- if $pos == length($input) and !$change; +      _input_pos($pos);  }  sub cmd_operator_y { -    my ($old_pos, $new_pos, $move) = @_; +    my ($old_pos, $new_pos, $move, $repeat) = @_;      my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move); +    # When yanking left of the current char, the current char is not included +    # in the yank. +    if ($old_pos > $new_pos) { +        $length--; +    } +      # Extract the selected string and put it in the " register.      my $input = _input(); -    $registers->{$register} = substr $input, $pos, $length; -    print "Yanked into $register: " . $registers->{$register} if DEBUG; +    my $string = substr $input, $pos, $length; +    if ($register =~ /[A-Z]/) { +        $registers->{lc $register} .= $string; +        print "Yanked into $register: ", $registers->{lc $register} if DEBUG; +    } else { +        $registers->{$register} = $string; +        print "Yanked into $register: ", $registers->{$register} if DEBUG; +    } -    _input_pos($old_pos); +    # Always move to the lower position. +    if ($old_pos > $new_pos) { +        _input_pos($new_pos); +    } else { +        _input_pos($old_pos); +    }  }  sub _get_pos_and_length {      my ($old_pos, $new_pos, $move) = @_; @@ -292,45 +544,52 @@ sub _get_pos_and_length {          $length *= -1;      } -    # w, x, X, h, l are the only movements which move one character after the -    # deletion area (which is what we need), all other commands need one -    # character more for correct deletion. -    if ($move ne 'w' and $move ne 'x' and $move ne 'X' and $move ne 'h' and $move ne 'l') { +    # Strip leading _a or _i if a text-object was used. +    if ($move =~ /^_[ai](.)/) { +        $move = $1; +    } + +    # Most movement commands don't move one character after the deletion area +    # (which is what we need). For those increase length to support proper +    # selection/deletion. +    if ($move ne 'w' and $move ne 'W' and $move ne 'x' and $move ne 'X' and +        $move ne 'B' and $move ne 'h' and $move ne 'l') {          $length += 1;      }      return ($old_pos, $length);  } +# COMMAND MODE COMMANDS -sub cmd_movement_h { -    my ($count, $pos) = @_; +sub cmd_h { +    my ($count, $pos, $repeat) = @_;      $pos -= $count;      $pos = 0 if $pos < 0; -    _input_pos($pos); +    return (undef, $pos);  } -sub cmd_movement_l { -    my ($count, $pos) = @_; +sub cmd_l { +    my ($count, $pos, $repeat) = @_;      my $length = _input_len();      $pos += $count; -    $pos = $length if $pos > $length; -    _input_pos($pos); +    $pos = _fix_input_pos($pos, $length); +    return (undef, $pos);  } -sub cmd_movement_space { -    my ($count, $pos) = @_; -    cmd_movement_l($count, $pos); +sub cmd_space { +    my ($count, $pos, $repeat) = @_; +    return cmd_l($count, $pos);  }  # later history (down) -sub cmd_movement_j { -    my ($count, $pos) = @_; +sub cmd_j { +    my ($count, $pos, $repeat) = @_;      if (Irssi::version < 20090117) {          # simulate a down-arrow          _emulate_keystrokes(0x1b, 0x5b, 0x42); -        return; +        return (undef, undef);      }      my @history = Irssi::active_win->get_history_lines(); @@ -338,8 +597,12 @@ sub cmd_movement_j {      if (defined $history_index) {          $history_index += $count;          print "History Index: $history_index" if DEBUG; +    # Prevent destroying the current input when pressing j after entering +    # command mode. Not exactly like in default irssi, but simplest solution +    # (and S can be used to clear the input line fast, which is what <down> +    # does in plain irssi).      } else { -        $history_index = $#history; +        return (undef, undef);      }      if ($history_index > $#history) { @@ -348,18 +611,24 @@ sub cmd_movement_j {          _input_pos($history_pos);          $history_index = $#history + 1;      } elsif ($history_index >= 0) { -        _input($history[$history_index]); +        my $history = $history[$history_index]; +        # History is not in UTF-8! +        if ($utf8) { +            $history = decode_utf8($history); +        } +        _input($history);          _input_pos(0);      } +    return (undef, undef);  }  # earlier history (up) -sub cmd_movement_k { -    my ($count, $pos) = @_; +sub cmd_k { +    my ($count, $pos, $repeat) = @_;      if (Irssi::version < 20090117) {          # simulate an up-arrow          _emulate_keystrokes(0x1b, 0x5b, 0x41); -        return; +        return (undef, undef);      }      my @history = Irssi::active_win->get_history_lines(); @@ -374,44 +643,101 @@ sub cmd_movement_k {      }      print "History Index: $history_index" if DEBUG;      if ($history_index >= 0) { -        _input($history[$history_index]); +        my $history = $history[$history_index]; +        # History is not in UTF-8! +        if ($utf8) { +            $history = decode_utf8($history); +        } +        _input($history);          _input_pos(0);      } +    return (undef, undef);  } +sub cmd_G { +    my ($count, $pos, $repeat) = @_; -sub cmd_movement_f { -    my ($count, $pos, $char) = @_; +    if (Irssi::version < 20090117) { +        _warn("G and gg not supported in irssi < 0.8.13"); +        return; +    } -    $pos = _next_occurrence(_input(), $char, $count, $pos); -    if ($pos != -1) { -        _input_pos($pos); +    my @history = Irssi::active_win->get_history_lines(); + +    # Go to the current input line if no count was given or it's too big. +    if (not $count or $count - 1 >= scalar @history) { +        if (defined $history_input and defined $history_pos) { +            _input($history_input); +            _input_pos($history_pos); +            $history_index = undef; +        } +        return; +    } else { +        # Save input line so it doesn't get lost. +        if (not defined $history_index) { +            $history_input = _input(); +            $history_pos = _input_pos(); +        } +        $history_index = $count - 1; +    } + +    my $history = $history[$history_index]; +    # History is not in UTF-8! +    if ($utf8) { +        $history = decode_utf8($history);      } +    _input($history); +    _input_pos(0); + +    return (undef, undef); +} +sub cmd_gg { +    my ($count, $pos, $repeat) = @_; + +    return cmd_G(1, $pos, $repeat); +} + +sub cmd_f { +    my ($count, $pos, $repeat, $char) = @_; + +    $pos = _next_occurrence(_input(), $char, $count, $pos); + +    $last_ftFT = { type => 'f', char => $char }; +    return (undef, $pos);  } -sub cmd_movement_t { -    my ($count, $pos, $char) = @_; +sub cmd_t { +    my ($count, $pos, $repeat, $char) = @_;      $pos = _next_occurrence(_input(), $char, $count, $pos); -    if ($pos != -1) { -        _input_pos($pos - 1); +    if (defined $pos) { +        $pos--;      } + +    $last_ftFT = { type => 't', char => $char }; +    return (undef, $pos);  } -sub cmd_movement_F { -    my ($count, $pos, $char) = @_; +sub cmd_F { +    my ($count, $pos, $repeat, $char) = @_;      my $input = reverse _input();      $pos = _next_occurrence($input, $char, $count, length($input) - $pos - 1); -    if ($pos != -1) { -        _input_pos(length($input) - $pos - 1); +    if (defined $pos) { +        $pos = length($input) - $pos - 1;      } + +    $last_ftFT = { type => 'F', char => $char }; +    return (undef, $pos);  } -sub cmd_movement_T { -    my ($count, $pos, $char) = @_; +sub cmd_T { +    my ($count, $pos, $repeat, $char) = @_;      my $input = reverse _input();      $pos = _next_occurrence($input, $char, $count, length($input) - $pos - 1); -    if ($pos != -1) { -        _input_pos(length($input) - $pos - 1 + 1); +    if (defined $pos) { +        $pos = length($input) - $pos - 1 + 1;      } + +    $last_ftFT = { type => 'T', char => $char }; +    return (undef, $pos);  }  # Find $count-th next occurrence of $char.  sub _next_occurrence { @@ -420,16 +746,85 @@ sub _next_occurrence {      while ($count-- > 0) {          $pos = index $input, $char, $pos + 1;          if ($pos == -1) { -            return -1; +            return undef;          }      }      return $pos;  } -sub cmd_movement_w { -    my ($count, $pos) = @_; +sub cmd_semicolon { +    my ($count, $pos, $repeat) = @_; + +    return (undef, undef) if not defined $last_ftFT->{type}; + +    (undef, $pos) +        = $commands->{$last_ftFT->{type}} +                   ->{func}($count, $pos, $repeat, $last_ftFT->{char}); +    return (undef, $pos); +} +sub cmd_comma { +    my ($count, $pos, $repeat) = @_; + +    return (undef, undef) if not defined $last_ftFT->{type}; + +    # Change direction. +    my $save = $last_ftFT->{type}; +    my $type = $save; +    $type =~ tr/ftFT/FTft/; + +    (undef, $pos) +        = $commands->{$type} +                   ->{func}($count, $pos, $repeat, $last_ftFT->{char}); +    # Restore type as the move functions overwrites it. +    $last_ftFT->{type} = $save; +    return (undef, $pos); +} + +sub cmd_w { +    my ($count, $pos, $repeat) = @_;      my $input = _input(); +    $pos = _beginning_of_word($input, $count, $pos); +    $pos = _fix_input_pos($pos, length $input); +    return (undef, $pos); +} +sub cmd_b { +    my ($count, $pos, $repeat) = @_; + +    my $input = reverse _input(); +    $pos = length($input) - $pos - 1; +    $pos = 0 if ($pos < 0); + +    $pos = _end_of_word($input, $count, $pos); +    $pos = length($input) - $pos - 1; +    $pos = 0 if ($pos < 0); +    return (undef, $pos); +} +sub cmd_e { +    my ($count, $pos, $repeat) = @_; + +    my $input = _input(); +    $pos = _end_of_word($input, $count, $pos); +    $pos = _fix_input_pos($pos, length $input); +    return (undef, $pos); +} +sub cmd_ge { +    my ($count, $pos, $repeat, $char) = @_; + +    my $input = reverse _input(); +    $pos = length($input) - $pos - 1; +    $pos = 0 if ($pos < 0); + +    $pos = _beginning_of_word($input, $count, $pos); +    $pos = length($input) - $pos - 1; +    $pos = 0 if ($pos < 0); + +    return (undef, $pos); +} +# Go to the beginning of $count-th word, like vi's w. +sub _beginning_of_word { +    my ($input, $count, $pos) = @_; +      while ($count-- > 0) {          # Go to end of next word/non-word.          if (substr($input, $pos) =~ /^$word+/ or @@ -443,25 +838,7 @@ sub cmd_movement_w {          }      } -    _input_pos($pos); -} -sub cmd_movement_b { -    my ($count, $pos) = @_; - -    my $input = reverse _input(); -    $pos = length($input) - $pos - 1; -    $pos = 0 if ($pos < 0); - -    $pos = _end_of_word($input, $count, $pos); -    $pos = length($input) - $pos - 1; -    $pos = 0 if ($pos < 0); -    _input_pos($pos); -} -sub cmd_movement_e { -    my ($count, $pos) = @_; - -    $pos = _end_of_word(_input(), $count, $pos); -    _input_pos($pos); +    return $pos;  }  # Go to the end of $count-th word, like vi's e.  sub _end_of_word { @@ -492,42 +869,76 @@ sub _end_of_word {          }      } +    # Necessary for correct deletion at the end of the line. +    if (length $input == $pos + 1) { +        $pos++; +    } +      return $pos;  } -sub cmd_movement_W { -    my ($count, $pos) = @_; +sub cmd_W { +    my ($count, $pos, $repeat) = @_;      my $input = _input(); -    while ($count-- > 0 and length($input) > $pos) { -        if (substr($input, $pos + 1) !~ /\s+/) { -            return cmd_movement_dollar(); -        } -        $pos += $+[0] + 1; -    } -    _input_pos($pos); +    $pos = _beginning_of_WORD($input, $count, $pos); +    $pos = _fix_input_pos($pos, length $input); +    return (undef, $pos);  } -sub cmd_movement_B { -    my ($count, $pos) = @_; +sub cmd_B { +    my ($count, $pos, $repeat) = @_;      my $input = reverse _input();      $pos = _end_of_WORD($input, $count, length($input) - $pos - 1);      if ($pos == -1) { -        cmd_movement_0(); +        return cmd_0();      } else { -        _input_pos(length($input) - $pos - 1); +        return (undef, length($input) - $pos - 1);      }  } -sub cmd_movement_E { -    my ($count, $pos) = @_; +sub cmd_E { +    my ($count, $pos, $repeat) = @_;      $pos = _end_of_WORD(_input(), $count, $pos);      if ($pos == -1) { -        cmd_movement_dollar(); +        return cmd_dollar();      } else { -        _input_pos($pos); +        return (undef, $pos);      }  } -# Go to the end of $count-th WORD, like vi's e. +sub cmd_gE { +    my ($count, $pos, $repeat, $char) = @_; + +    my $input = reverse _input(); +    $pos = _beginning_of_WORD($input, $count, length($input) - $pos - 1); +    if ($pos == -1 or length($input) - $pos - 1 == -1) { +        return cmd_0(); +    } else { +        $pos = length($input) - $pos - 1; +    } + +    return (undef, $pos); +} +# Go to beginning of $count-th WORD, like vi's W. +sub _beginning_of_WORD { +    my ($input, $count, $pos) = @_; + +    # Necessary for correct movement between two words with only one +    # whitespace. +    if (substr($input, $pos) =~ /^\s\S/) { +        $pos++; +        $count--; +    } + +    while ($count-- > 0 and length($input) > $pos) { +        if (substr($input, $pos + 1) !~ /\s+/) { +            return length($input); +        } +        $pos += $+[0] + 1; +    } + +    return $pos; +} +# Go to end of $count-th WORD, like vi's E.  sub _end_of_WORD {      my ($input, $count, $pos) = @_; @@ -548,82 +959,237 @@ sub _end_of_WORD {      return $pos;  } -sub cmd_movement_0 { -    _input_pos(0); +sub cmd__i { +    my ($count, $pos, $repeat, $char) = @_; + +    _warn("i_ not implemented yet"); +    return (undef, undef); +} +sub cmd__a { +    my ($count, $pos, $repeat, $char) = @_; + +    my $cur_pos; +    my $input = _input(); + +    # aw and aW +    if ($char eq 'w' or $char eq 'W') { +        while ($count-- > 0 and length($input) > $pos) { +            if (substr($input, $pos, 1) =~ /\s/) { +                # Any whitespace before the word/WORD must be removed. +                if (not defined $cur_pos) { +                    $cur_pos = _find_regex_before($input, '\S', $pos, 0); +                    if ($cur_pos < 0) { +                        $cur_pos = 0; +                    } else { +                        $cur_pos++; +                    } +                } +                # Move before the word/WORD. +                if (substr($input, $pos + 1) =~ /^\s+/) { +                    $pos += $+[0]; +                } +                # And delete the word. +                if ($char eq 'w') { +                    if (substr($input, $pos) =~ /^\s($word+|$non_word+)/) { +                        $pos += $+[0]; +                    } else { +                        $pos = length($input); +                    } +                # WORD +                } else { +                    if (substr($input, $pos + 1) =~ /\s/) { +                        $pos += $-[0] + 1; +                    } else { +                        $pos = length($input); +                    } +                } + +            # word +            } elsif ($char eq 'w') { +                # Start at the beginning of this WORD. +                if (not defined $cur_pos and $pos > 0 and substr($input, $pos - 1, 2) !~ /(\s.|$word$non_word|$non_word$word)/) { + +                    $cur_pos = _find_regex_before($input, "^($word+$non_word|$non_word+$word|$word+\\s|$non_word+\\s)", $pos, 1); +                    if ($cur_pos < 0) { +                        $cur_pos = 0; +                    } else { +                        $cur_pos += 2; +                    } +                } +                # Delete to the end of the word. +                if (substr($input, $pos) =~ /^($word+$non_word|$non_word+$word|$word+\s+\S|$non_word+\s+\S)/) { +                    $pos += $+[0] - 1; +                } else { +                    $pos = length($input); +                    # If we are at the end of the line, whitespace before +                    # the word is also deleted. +                    my $new_pos = _find_regex_before($input, "^($word+\\s+|$non_word+\\s+)", $pos, 1); +                    if ($new_pos != -1 and (not defined $cur_pos or $cur_pos > $new_pos + 1)) { +                        $cur_pos = $new_pos + 1; +                    } +                } + +            # WORD +            } else { +                # Start at the beginning of this WORD. +                if (not defined $cur_pos and $pos > 0 and +                        substr($input, $pos - 1, 1) !~ /\s/) { +                    $cur_pos = _find_regex_before($input, '\s', $pos - 1, 0); +                    if ($cur_pos < 0) { +                        $cur_pos = 0; +                    } else { +                        $cur_pos++; +                    } +                } +                # Delete to the end of the word. +                if (substr($input, $pos + 1) =~ /^\S*\s+\S/) { +                    $pos += $+[0]; +                } else { +                    $pos = length($input); +                    # If we are at the end of the line, whitespace before +                    # the WORD is also deleted. +                    my $new_pos = _find_regex_before($input, '\s+', $pos, 1); +                    if (not defined $cur_pos or $cur_pos > $new_pos + 1) { +                        $cur_pos = $new_pos + 1; +                    } +                } +            } +        } +    } + +    return ($cur_pos, $pos); +} +# Find regex as close as possible before the current position. If $end is true +# the end of the match is returned, otherwise the beginning. +sub _find_regex_before { +    my ($input, $regex, $pos, $end) = @_; + +    $input = reverse $input; +    $pos = length($input) - $pos - 1; +    $pos = 0 if $pos < 0; + +    if (substr($input, $pos) =~ /$regex/) { +        if (!$end) { +            $pos += $-[0]; +        } else { +            $pos += $+[0]; +        } +        return length($input) - $pos - 1; +    } else { +        return -1; +    } +} + +sub cmd_0 { +    return (undef, 0);  } -sub cmd_movement_caret { +sub cmd_caret {      my $input = _input();      my $pos;      # No whitespace at all.      if ($input !~ m/^\s/) {          $pos = 0; -    # Some non-whitesapece, go to first one. +    # Some non-whitespace, go to first one.      } elsif ($input =~ m/[^\s]/) {          $pos = $-[0];      # Only whitespace, go to the end.      } else { -        $pos = _input_len(); +        $pos = _fix_input_pos(length $input, length $input);      } -    _input_pos($pos); +    return (undef, $pos);  } -sub cmd_movement_dollar { -    _input_pos(_input_len()); +sub cmd_dollar { +    my $length = _input_len(); +    return (undef, _fix_input_pos($length, $length));  } -sub cmd_movement_x { -    my ($count, $pos) = @_; +sub cmd_x { +    my ($count, $pos, $repeat) = @_;      cmd_operator_d($pos, $pos + $count, 'x'); +    return (undef, undef);  } -sub cmd_movement_X { -    my ($count, $pos) = @_; +sub cmd_X { +    my ($count, $pos, $repeat) = @_; -    return if $pos == 0; +    return (undef, undef) if $pos == 0;      my $new = $pos - $count;      $new = 0 if $new < 0;      cmd_operator_d($pos, $new, 'X'); +    return (undef, undef);  } +sub cmd_s { +    my ($count, $pos, $repeat) = @_; -sub cmd_movement_i { -    _update_mode(M_INS); +    $operator = $commands->{c}; +    return (undef, $pos + 1);  } -sub cmd_movement_I { -    cmd_movement_caret(); -    _update_mode(M_INS); +sub cmd_S { +    my ($count, $pos, $repeat) = @_; + +    $operator = $commands->{c}; +    return (0, _input_len());  } -sub cmd_movement_a { -    cmd_movement_l(1, _input_pos()); -    _update_mode(M_INS); + +sub cmd_i { +    my ($count, $pos, $repeat) = @_; + +    if (!$repeat) { +        _update_mode(M_INS); +    } else { +        $pos = _insert_buffer($count, $pos); +    } +    return (undef, $pos);  } -sub cmd_movement_A { -    cmd_movement_dollar(); -    _update_mode(M_INS); +sub cmd_I { +    my ($count, $pos, $repeat) = @_; + +    $pos = cmd_caret(); +    if (!$repeat) { +        _update_mode(M_INS); +    } else { +        $pos = _insert_buffer($count, $pos); +    } +    return (undef, $pos);  } +sub cmd_a { +    my ($count, $pos, $repeat) = @_; -sub cmd_movement_r { -    my ($count, $pos, $char) = @_; +    # Move after current character. Can't use cmd_l() because we need to mover +    # after last character at the end of the line. +    my $length = _input_len(); +    $pos += 1; +    $pos = $length if $pos > $length; -    my $input = _input(); -    substr $input, $pos, 1, $char; -    _input($input); -    _input_pos($pos); +    if (!$repeat) { +        _update_mode(M_INS); +    } else { +        $pos = _insert_buffer($count, $pos); +    } +    return (undef, $pos);  } +sub cmd_A { +    my ($count, $pos, $repeat) = @_; -sub cmd_movement_p { -    my ($count, $pos) = @_; -    _paste_at_position($count, $pos + 1); +    $pos = _input_len(); + +    if (!$repeat) { +        _update_mode(M_INS); +    } else { +        $pos = _insert_buffer($count, $pos); +    } +    return (undef, $pos);  } -sub cmd_movement_P { +# Add @insert_buf to _input() at the given position. +sub _insert_buffer {      my ($count, $pos) = @_; -    _paste_at_position($count, $pos); +    return _insert_at_position(join('', @insert_buf), $count, $pos);  } -sub _paste_at_position { -    my ($count, $pos) = @_; +sub _insert_at_position { +    my ($string, $count, $pos) = @_; -    return if not $registers->{$register}; - -    my $string = $registers->{$register} x $count; +    $string = $string x $count;      my $input = _input();      # Check if we are not at the end of the line to prevent substr outside of @@ -635,45 +1201,212 @@ sub _paste_at_position {      }      _input($input); -    _input_pos($pos - 1 + length $string); +    return $pos - 1 + length $string;  } -sub cmd_movement_G { +sub cmd_r { +    my ($count, $pos, $repeat, $char) = @_; + +    my $input = _input(); + +    # Abort if at end of the line. +    return (undef, undef) if length($input) < $pos + $count; + +    substr $input, $pos, $count, $char x $count; +    _input($input); +    return (undef, $pos + $count - 1); +} + +sub cmd_p { +    my ($count, $pos, $repeat) = @_; +    $pos = _paste_at_position($count, $pos + 1); +    return (undef, $pos); +} +sub cmd_P { +    my ($count, $pos, $repeat) = @_; +    $pos = _paste_at_position($count, $pos); +    return (undef, $pos); +} +sub _paste_at_position {      my ($count, $pos) = @_; -    # If no count is given go to the last window (= highest refnum). -    if (not $count) { -        $count = List::Util::max(map { $_->{refnum} } Irssi::windows()); +    return if not $registers->{$register}; +    return _insert_at_position($registers->{$register}, $count, $pos); +} + +sub cmd_C { +    my ($count, $pos, $repeat) = @_; + +    $operator = $commands->{c}; +    return (undef, _input_len()); +} +sub cmd_D { +    my ($count, $pos, $repeat) = @_; + +    $operator = $commands->{d}; +    return (undef, _input_len()); +} + +sub cmd_ctrl_d { +    my ($count, $pos, $repeat) = @_; + +    my $window = Irssi::active_win(); +    # no count = half of screen +    if (not defined $count) { +        $count = $window->{height} / 2;      } +    $window->view()->scroll($count); +    return (undef, undef); +} +sub cmd_ctrl_u { +    my ($count, $pos, $repeat) = @_; -    my $window = Irssi::window_find_refnum($count); -    if ($window) { -        $window->set_active(); +    my $window = Irssi::active_win(); +    # no count = half of screen +    if (not defined $count) { +        $count = $window->{height} / 2;      } +    $window->view()->scroll($count * -1); +    return (undef, undef);  } +sub cmd_ctrl_f { +    my ($count, $pos, $repeat) = @_; -sub cmd_movement_tilde { -    my ($count, $pos) = @_; +    my $window = Irssi::active_win(); +    $window->view()->scroll($count * $window->{height}); +    return (undef, undef); +} +sub cmd_ctrl_b { +    my ($count, $pos, $repeat) = @_; + +    cmd_ctrl_f($count * -1, $pos, $repeat); +    return (undef, undef); +} + +sub cmd_ctrl_wj { +    my ($count, $pos, $repeat) = @_; + +    while ($count-- > 0) { +        Irssi::command('window down'); +    } + +    return (undef, undef); +} +sub cmd_ctrl_wk { +    my ($count, $pos, $repeat) = @_; + +    while ($count-- > 0) { +        Irssi::command('window up'); +    } + +    return (undef, undef); +} +sub cmd_ctrl_6 { +    # like :b# +    Irssi::command('window last'); +    return (undef, undef); +} + +sub cmd_tilde { +    my ($count, $pos, $repeat) = @_;      my $input = _input();      my $string = substr $input, $pos, $count; -    $string =~ tr/a-zA-Z/A-Za-z/; +    $string =~ s/(.)/(uc($1) eq $1) ? lc($1) : uc($1)/ge;      substr $input, $pos, $count, $string;      _input($input); -    _input_pos($pos + $count); +    return (undef, _fix_input_pos($pos + $count, length $input));  } -sub cmd_movement_register { -    my ($count, $pos, $char) = @_; +sub cmd_register { +    my ($count, $pos, $repeat, $char) = @_; + +    if (not exists $registers->{$char} and not exists $registers->{lc $char}) { +        print "Wrong register $char, ignoring." if DEBUG; +        return (undef, undef); +    } + +    # make sure black hole register is always empty +    if ($char eq '_') { +        $registers->{_} = ''; +    } + +    # + and * contain both irssi's cut-buffer +    if ($char eq '+' or $char eq '*') { +        $registers->{'+'} = Irssi::parse_special('$U'); +        $registers->{'*'} = $registers->{'+'}; +    }      $register = $char;      print "Changing register to $register" if DEBUG; +    return (undef, undef);  } +sub cmd_undo { +    print "Undo!" if DEBUG; + +    if ($undo_index != $#undo_buffer) { +        $undo_index++; +        _restore_undo_entry($undo_index); +        print "Undoing entry index: $undo_index of " . scalar(@undo_buffer) +            if DEBUG; +    } else { +        print "No further undo." if DEBUG; +    } +    return (undef, undef); +} +sub cmd_redo { +    print "Redo!" if DEBUG; + +    if ($undo_index != 0) { +        $undo_index--; +        print "Undoing entry index: $undo_index of " . scalar(@undo_buffer) +            if DEBUG; +        _restore_undo_entry($undo_index); +    } else { +        print "No further Redo." if DEBUG; +    } +    return (undef, undef); +} + +# Adapt the input position depending if an operator is active or not. +sub _fix_input_pos { +    my ($pos, $length) = @_; + +    # Allow moving past the last character when an operator is active to allow +    # correct handling of last character in line. +    if ($operator) { +        $pos = $length if $pos > $length; +    # Otherwise forbid it. +    } else { +        $pos = $length - 1 if $pos > $length - 1; +    } + +    return $pos; +} + + +# EX MODE COMMANDS +  sub cmd_ex_command {      my $arg_str = join '', @ex_buf; -    if ($arg_str =~ m|s/(.+)/(.*)/([ig]*)|) { + +    if ($arg_str !~ /^([a-z]+)/) { +        return _warn("Invalid Ex-mode command!"); +    } + +    if (not exists $commands_ex->{$1}) { +        return _warn("Ex-mode $1 doesn't exist!"); +    } + +    $commands_ex->{$1}->{func}($arg_str); +} + +sub ex_substitute { +    my ($arg_str) = @_; + +    if ($arg_str =~ m|^s/(.+)/(.*)/([ig]*)|) {          my ($search, $replace, $flags) = ($1, $2, $3);          print "Searching for $search, replace: $replace, flags; $flags"            if DEBUG; @@ -681,7 +1414,7 @@ sub cmd_ex_command {          my $rep_fun = sub { $replace };          my $line = _input(); -        my @re_flags = split '', $flags // ''; +        my @re_flags = split '', defined $flags?$flags:'';          if (scalar grep { $_ eq 'i' } @re_flags) {              $search = '(?i)' . $search; @@ -700,9 +1433,149 @@ sub cmd_ex_command {          print "New line is: $line" if DEBUG;          _input($line); +    } else { +        _warn_ex('s'); +    } +} + +sub ex_bnext { +    Irssi::command('window next'); +} +sub ex_bprev { +    Irssi::command('window previous'); +} +sub ex_bdelete { +    Irssi::command('window close'); +} +sub ex_buffer { +    my ($arg_str) = @_; + +    # :b[buffer] {args} +    if ($arg_str =~ m|^b(?:uffer)?\s*(.+)$|) { +        my $window; +        my $item; +        my $buffer = $1; + +        # Go to window number. +        if ($buffer =~ /^[0-9]+$/) { +            $window = Irssi::window_find_refnum($buffer); +        # Go to previous window. +        } elsif ($buffer eq '#') { +            Irssi::command('window last'); +        # Go to best regex matching window. +        } else { +            my $matches = _matching_windows($buffer); +            if (scalar @$matches > 0) { +                $window = @$matches[0]->{window}; +                $item = @$matches[0]->{item}; +            } +        } + +        if ($window) { +            $window->set_active(); +            if ($item) { +                $item->set_active(); +            } +        } +    } else { +        _warn_ex('buffer');      }  } +sub ex_registers { +    my ($arg_str) = @_; + +    # :reg[isters] {arg} and :di[splay] {arg} +    if ($arg_str =~ /^(?:reg(?:isters)?|di(?:splay)?)(?:\s+(.+)$)?/) { +        my @regs; +        if ($1) { +            my $regs = $1; +            $regs =~ s/\s+//g; +            @regs = split //, $regs; +        } else { +            @regs = keys %$registers; +        } + +        # Update "+ and "* registers so correct values are displayed. +        $registers->{'+'} = Irssi::parse_special('$U'); +        $registers->{'*'} = $registers->{'+'}; + +        my $active_window = Irssi::active_win; +        foreach my $key (sort @regs) { +            next if $key eq '_'; # skip black hole +            if (defined $registers->{$key}) { +                $active_window->print("register $key: $registers->{$key}"); +            } +        } +    } else { +        _warn_ex(':registers'); +    } +} + +sub ex_buffers { +    Irssi::command('window list'); +} + +sub ex_undolist { +    _print_undo_buffer(); +} + +sub _warn_ex { +    my ($command) = @_; +    _warn("Error in ex-mode command $command"); +} + +sub _matching_windows { +    my ($buffer) = @_; + +    my $server; + +    if ($buffer =~ m{^(.+)/(.+)}) { +        $server = $1; +        $buffer = $2; +    } + +    print ":b searching for channel $buffer" if DEBUG; +    print ":b on server $server" if $server and DEBUG; + +    my @matches; +    foreach my $window (Irssi::windows()) { +        # Matching window names. +        if ($window->{name} =~ /$buffer/i) { +            my $win_ratio = ($+[0] - $-[0]) / length($window->{name}); +            push @matches, { window => $window, +                               item => undef, +                              ratio => $win_ratio, +                               text => $window->{name} }; +            print ":b $window->{name}: $win_ratio" if DEBUG; +        } +        # Matching Window item names (= channels). +        foreach my $item ($window->items()) { +            # Wrong server. +            if ($server and (!$item->{server} or +                              $item->{server}->{chatnet} !~ /^$server/i)) { +                next; +            } +            if ($item->{name} =~ /$buffer/i) { +                my $length = length($item->{name}); +                $length-- if index($item->{name}, '#') == 0; +                my $item_ratio = ($+[0] - $-[0]) / $length; +                push @matches, { window => $window, +                                   item => $item, +                                  ratio => $item_ratio, +                                   text => $item->{name} }; +                print ":b $window->{name} $item->{name}: $item_ratio" if DEBUG; +            } +        } +    } + +    @matches = sort {$b->{ratio} <=> $a->{ratio}} @matches; + +    return \@matches; +} + + +# STATUS ITEMS  # vi mode status item.  sub vim_mode_cb { @@ -723,10 +1596,10 @@ sub vim_mode_cb {                  $mode_str .= $numeric_prefix;              }              if ($operator) { -                $mode_str .= $operator; +                $mode_str .= $operator->{char};              }              if ($movement) { -                $mode_str .= $movement; +                $mode_str .= $movement->{char};              }              $mode_str .= ')';          } @@ -734,6 +1607,28 @@ sub vim_mode_cb {      $sb_item->default_handler($get_size_only, "{sb $mode_str}", '', 0);  } +# :b window list item. +sub b_windows_cb { +    my ($sb_item, $get_size_only) = @_; + +    my $windows = ''; + +    # A little code duplication of cmd_ex_command()! +    my $arg_str = join '', @ex_buf; +    if ($arg_str =~ m|^b(?:uffer)?\s*(.+)$|) { +        my $buffer = $1; +        if ($buffer !~ /^[0-9]$/ and $buffer ne '#') { +            # Display matching windows. +            my $matches = _matching_windows($buffer); +            $windows = join ',', map { $_->{text} } @$matches; +        } +    } + +    $sb_item->default_handler($get_size_only, "{sb $windows}", '', 0); +} + + +# INPUT HANDLING  sub got_key {      my ($key) = @_; @@ -750,23 +1645,21 @@ sub got_key {          # contain escape sequences (arrow keys, etc)          $input_buf_timer            = Irssi::timeout_add_once(10, \&handle_input_buffer, undef); - +        print "Buffer Timer tag: $input_buf_timer" if DEBUG;      } elsif ($mode == M_INS) {          if ($key == 3) { # Ctrl-C enter command mode              _update_mode(M_CMD);              _stop();              return; +          } elsif ($key == 10) { # enter. -            _stop(); -            _commit_line(_input()); -            @undo_buffer = (); -            $undo_index = undef; +            _commit_line();          } elsif ($input_buf_enabled and $imap) {              print "Imap $imap active" if DEBUG;              my $map = $imaps->{$imap}; -            if (chr($key) eq $map->{map}) { -                $map->{func}(); +            if (not defined $map->{map} or chr($key) eq $map->{map}) { +                $map->{func}($key);                  # Clear the buffer so the imap is not printed.                  @input_buf = ();              } else { @@ -776,6 +1669,7 @@ sub got_key {              _stop();              $imap = undef;              return; +          } elsif (exists $imaps->{chr($key)}) {              print "Imap " . chr($key) . " seen, starting buffer" if DEBUG; @@ -785,10 +1679,19 @@ sub got_key {              $input_buf_enabled = 1;              push @input_buf, $key;              $input_buf_timer -              = Irssi::timeout_add_once(500, \&flush_input_buffer, undef); +              = Irssi::timeout_add_once(1000, \&flush_input_buffer, undef);              _stop();              return; + +        # Pressing delete resets insert mode repetition. +        # TODO: maybe allow it +        } elsif ($key == 127) { +            @insert_buf = (); +        # All other entered characters need to be stored to allow repeat of +        # insert mode. Ignore delete and ctrl characters. +        } elsif ($key > 31) { +            push @insert_buf, chr($key);          }      } @@ -798,14 +1701,19 @@ sub got_key {          return;      } -    if ($mode == M_CMD || $mode == M_EX) { -        handle_command($key); +    if ($mode == M_CMD) { +        my $should_stop = handle_command_cmd($key); +        _stop() if $should_stop; +        Irssi::statusbar_items_redraw("vim_mode"); + +    } elsif ($mode == M_EX) { +        handle_command_ex($key);      }  }  sub handle_input_buffer { -    Irssi::timeout_remove($input_buf_timer); +    #Irssi::timeout_remove($input_buf_timer);      $input_buf_timer = undef;      # see what we've collected.      print "Input buffer contains: ", join(", ", @input_buf) if DEBUG; @@ -818,17 +1726,22 @@ sub handle_input_buffer {      } else {          # we need to identify what we got, and either replay it          # or pass it off to the command handler. -        if ($mode == M_CMD) { -            # command -            my $key_str = join '', map { chr } @input_buf; -            if ($key_str =~ m/^\e\[([ABCD])/) { -                print "Arrow key: $1" if DEBUG; -            } else { -                print "Dunno what that is." if DEBUG; -            } -        } else { -            _emulate_keystrokes(@input_buf); -        } +        # if ($mode == M_CMD) { +        #     # command +        #     my $key_str = join '', map { chr } @input_buf; +        #     if ($key_str =~ m/^\e\[([ABCD])/) { +        #         print "Arrow key: $1" if DEBUG; +        #     } else { +        #         print "Dunno what that is." if DEBUG; +        #     } +        # } else { +        #     _emulate_keystrokes(@input_buf); +        # } +        _emulate_keystrokes(@input_buf); + +        # Clear insert buffer, pressing "special" keys (like arrow keys) +        # resets it. +        @insert_buf = ();      }      @input_buf = (); @@ -841,6 +1754,9 @@ sub flush_input_buffer {      # see what we've collected.      print "Input buffer flushed" if DEBUG; +    # Add the characters to @insert_buf so they can be repeated. +    push @insert_buf, map chr, @input_buf; +      _emulate_keystrokes(@input_buf);      @input_buf = (); @@ -849,6 +1765,18 @@ sub flush_input_buffer {      $imap = undef;  } +sub flush_pending_map { +    my ($old_pending_map) = @_; + +    print "flush_pending_map(): ", $pending_map, ' ', $old_pending_map +        if DEBUG; + +    return if not defined $pending_map or +              $pending_map ne $old_pending_map; + +    handle_command_cmd(undef); +} +  sub handle_numeric_prefix {      my ($char) = @_;      my $num = 0+$char; @@ -861,198 +1789,308 @@ sub handle_numeric_prefix {      }  } -sub handle_command { +sub handle_command_cmd {      my ($key) = @_; -    if ($mode == M_EX) { -        # DEL key - remove last character -        if ($key == 127) { -            print "Delete" if DEBUG; -            pop @ex_buf; -            _set_prompt(':' . join '', @ex_buf); - -        # Return key - execute command -        } elsif ($key == 10) { -            print "Run ex-mode command" if DEBUG; -            cmd_ex_command(); -            _set_prompt(''); -            @ex_buf = (); -            _update_mode(M_CMD); +    my $pending_map_flushed = 0; -        # Append entered key -        } else { -            push @ex_buf, chr $key; -            _set_prompt(':' . join '', @ex_buf); +    my $char; +    if (defined $key) { +        $char = chr($key); +    # We were called from flush_pending_map(). +    } else { +        $char = $pending_map; +        $key = 0; +        $pending_map_flushed = 1; +    } + +    # Counts +    if (!$movement and ($char =~ m/[1-9]/ or +                        ($numeric_prefix && $char =~ m/[0-9]/))) { +        print "Processing numeric prefix: $char" if DEBUG; +        handle_numeric_prefix($char); +        return 1; # call _stop() +    } + +    if (defined $pending_map and not $pending_map_flushed) { +        $pending_map = $pending_map . $char; +        $char = $pending_map; +    } + +    my $map; +    if ($movement) { +        $map = { char => $movement->{char}, +                 cmd => $movement, +                 maps => {}, +               }; + +    } elsif (exists $maps->{$char}) { +        $map = $maps->{$char}; + +        # We have multiple mappings starting with this key sequence. +        if (!$pending_map_flushed and scalar keys %{$map->{maps}} > 0) { +            if (not defined $pending_map) { +                $pending_map = $char; +            } + +            # The current key sequence has a command mapped to it, run if +            # after a timeout. +            if (defined $map->{cmd}) { +                Irssi::timeout_add_once(1000, \&flush_pending_map, +                                              $pending_map); +            } +            return 1; # call _stop()          }      } else { -        my $char = chr($key); +        print "No mapping found for $char" if DEBUG; +        $pending_map = undef; +        return 1; # call _stop() +    } -        # We need to treat $movements_multiple specially as they need another -        # argument. -        if ($movement) { -            $movement .= $char; -        } +    $pending_map = undef; -        # S is an alias for cc. -        if (!$movement and !$operator and $char eq 'S') { -            print "Changing S to cc" if DEBUG; -            $char = 'c'; -            $operator = 'c'; -        } +    my $cmd = $map->{cmd}; -        if (!$movement && ($char =~ m/[1-9]/ || -                           ($numeric_prefix && $char =~ m/[0-9]/))) { -            print "Processing numeric prefix: $char" if DEBUG; -            handle_numeric_prefix($char); +    # Make sure we have a valid $cmd. +    if (not defined $cmd) { +        print "Bug in pending_map_flushed() $map->{char}" if DEBUG; +        return 1; # call _stop() +    } -        } elsif (!$movement && exists $movements_multiple->{$char}) { -            print "Processing movement: $char" if DEBUG; -            $movement = $char; +    # Ex-mode commands can also be bound in command mode. Works only if the +    # ex-mode command doesn't take any arguments. +    if ($cmd->{type} == C_EX) { +        $cmd->{func}->(); +        return 1; # call _stop() +    } -        } elsif (!$movement && exists $operators->{$char}) { -            print "Processing operator: $char" if DEBUG; +    # text-objects (i a) are simulated with $movement +    if (!$movement and ($cmd->{type} == C_NEEDSKEY or +                        ($operator and ($char eq 'i' or $char eq 'a')))) { +        print "Processing movement: $map->{char} ($cmd->{char})" if DEBUG; +        if ($char eq 'i') { +            $movement = $commands->{_i}; +        } elsif ($char eq 'a') { +            $movement = $commands->{_a}; +        } else { +            $movement = $cmd; +        } -            # Abort operator if we already have one pending. -            if ($operator) { -                # But allow cc/dd/yy. -                if ($operator eq $char) { -                    print "Processing operator: ", $operator, $char if DEBUG; -                    my $pos = _input_pos(); -                    $operators->{$operator}->{func}->(0, _input_len(), ''); -                    # Restore position for yy. -                    if ($char eq 'y') { -                        _input_pos($pos); -                    } +    } elsif (!$movement and $cmd->{type} == C_OPERATOR) { +        print "Processing operator: $map->{char} ($cmd->{char})" if DEBUG; +        # Abort operator if we already have one pending. +        if ($operator) { +            # But allow cc/dd/yy. +            if ($operator == $cmd) { +                print "Processing line operator: $map->{char} ($cmd->{char})" +                    if DEBUG; +                my $pos = _input_pos(); +                $cmd->{func}->(0, _input_len(), '', 0); +                # Restore position for yy. +                if ($cmd == $commands->{y}) { +                    _input_pos($pos); +                } +                if ($register ne '"') { +                    print 'Changing register to "' if DEBUG; +                    $register = '"';                  } -                $numeric_prefix = undef; -                $operator = undef; -                $movement = undef; -            # Set new operator. -            } else { -                $operator = $char;              } +            $numeric_prefix = undef; +            $operator = undef; +            $movement = undef; +        # Set new operator. +        } else { +            $operator = $cmd; +        } -        } elsif ($movement || exists $movements->{$char}) { -            print "Processing movement command: $char" if DEBUG; - -            my $skip = 0; +    # Start Ex mode. +    } elsif ($cmd == $commands->{':'}) { +        if (not script_is_loaded('prompt_info')) { +            _warn("Warning: Ex mode requires the 'prompt_info' script. " . +                    "Please load it and try again."); +        } else { +            _update_mode(M_EX); +            _set_prompt(':'); +        } -            if (!$movement) { -                # . repeats the last command. -                if ($char eq '.' and defined $last->{char}) { -                    $char = $last->{char}; -                    # If . is given a count then it replaces original count. -                    if (not defined $numeric_prefix) { -                        $numeric_prefix = $last->{numeric_prefix}; -                    } -                    $operator = $last->{operator}; -                    $movement = $last->{movement}; -                    $register = $last->{register}; -                } elsif ($char eq '.') { -                    $skip = 1; -                } -                # C and D force the matching operator -                if ($char eq 'C') { -                    $operator = 'c'; -                } elsif ($char eq 'D') { -                    $operator = 'd'; +    # Enter key sends the current input line in command mode as well. +    } elsif ($key == 10) { +        _commit_line(); +        return 0; # don't call _stop() + +    } else { #if ($movement || exists $movements->{$char}) { +        print "Processing command: $map->{char} ($cmd->{char})" if DEBUG; + +        my $skip = 0; +        my $repeat = 0; + +        if (!$movement) { +            # . repeats the last command. +            if ($cmd == $commands->{'.'} and defined $last->{cmd}) { +                $cmd = $last->{cmd}; +                $char = $last->{char}; +                # If . is given a count then it replaces original count. +                if (not defined $numeric_prefix) { +                    $numeric_prefix = $last->{numeric_prefix};                  } +                $operator = $last->{operator}; +                $movement = $last->{movement}; +                $register = $last->{register}; +                $repeat = 1; +            } elsif ($cmd == $commands->{'.'}) { +                print '. pressed but $last->{char} not set' if DEBUG; +                $skip = 1;              } +        } + +        if ($skip) { +            print "Skipping movement and operator." if DEBUG; +        } else { +            # Make sure count is at least 1 except for functions which need to +            # know if no count was used. +            if (not $numeric_prefix and not $cmd->{needs_count}) { +                $numeric_prefix = 1; +            } + +            my $cur_pos = _input_pos(); -            if ($skip) { -                print "Skipping movement and operator." if DEBUG; +            # If defined $cur_pos will be changed to this. +            my $old_pos; +            # Position after the move. +            my $new_pos; +            # Execute the movement (multiple times). +            if (not $movement) { +                ($old_pos, $new_pos) +                    = $cmd->{func}->($numeric_prefix, $cur_pos, $repeat);              } else { -                # Make sure count is at least 1, except for G which needs to -                # handle undef specially. -                if (not $numeric_prefix and $char ne 'G') { -                    $numeric_prefix = 1; -                } +                ($old_pos, $new_pos) +                    = $cmd->{func}->($numeric_prefix, $cur_pos, $repeat, +                                     $char); +            } +            if (defined $old_pos) { +                print "Changing \$cur_pos from $cur_pos to $old_pos" if DEBUG; +                $cur_pos = $old_pos; +            } +            if (defined $new_pos) { +                _input_pos($new_pos); +            } else { +                $new_pos = _input_pos(); +            } -                # Execute the movement (multiple times). -                my $cur_pos = _input_pos(); -                if (not $movement) { -                    $movements->{$char}->{func}->($numeric_prefix, $cur_pos); -                } else { -                    # Use the real movement command (like t or f) for operator -                    # below. -                    $char = substr $movement, 0, 1; -                    $movements->{$char}->{func} -                              ->($numeric_prefix, $cur_pos, substr $movement, 1); -                } -                my $new_pos = _input_pos(); - -                # If we have an operator pending then run it on the handled -                # text. But only if the movement changed the position (this -                # prevents problems with e.g. f when the search string doesn't -                # exist). -                if ($operator and $cur_pos != $new_pos) { -                    print "Processing operator: ", $operator if DEBUG; -                    $operators->{$operator}->{func}->($cur_pos, $new_pos, $char); -                } +            # Update input position of last undo entry so that undo/redo +            # restores correct position. +            if (@undo_buffer and _input() eq $undo_buffer[0]->[0] and +                ((defined $operator and $operator == $commands->{d}) or +                 $cmd->{repeatable})) { +                print "Updating history position: $undo_buffer[0]->[0]" +                    if DEBUG; +                $undo_buffer[0]->[1] = $cur_pos; +            } -                # Store command, necessary for . But ignore movements and -                # registers. -                if ($operator or $char eq 'x' or $char eq 'X' or $char eq 'r' -                              or $char eq 'p' or $char eq 'P' or -                                 $char eq 'C' or $char eq 'D' or -                                 $char eq '~' or $char eq '"') { -                    $last->{char} = $char; -                    $last->{numeric_prefix} = $numeric_prefix; -                    $last->{operator} = $operator; -                    $last->{movement} = $movement; -                    $last->{register} = $register; +            # If we have an operator pending then run it on the handled text. +            # But only if the movement changed the position (this prevents +            # problems with e.g. f when the search string doesn't exist). +            if ($operator and $cur_pos != $new_pos) { +                print "Processing operator: ", $operator->{char} if DEBUG; +                # If text-objects are used the real move character must also +                # be passed to the operator. +                my $tmp_char = $cmd->{char}; +                if ($tmp_char eq '_i' or $tmp_char eq '_a') { +                   $tmp_char .= $char;                  } +                $operator->{func}->($cur_pos, $new_pos, $tmp_char, $repeat);              } -            $numeric_prefix = undef; -            $operator = undef; -            $movement = undef; +            # Save an undo checkpoint here for operators, all repeatable +            # movements, operators and repetition. +            if ((defined $operator and $operator == $commands->{d}) or +                $cmd->{repeatable}) { +                # TODO: why do histpry entries still show up in undo +                # buffer? Is avoiding the commands here insufficient? -            if ($char ne '"' and $register ne '"') { -                print 'Changing register to "' if DEBUG; -                $register = '"'; +                _add_undo_entry(_input(), _input_pos());              } -        # Start Ex mode. -        } elsif ($char eq ':') { -            if (not script_is_loaded('prompt_info')) { -                print "This script requires the 'prompt_info' script to " -                    . "support Ex mode. Please load it and try again."; -            } else { -                _update_mode(M_EX); -                _set_prompt(':'); +            # Store command, necessary for . +            if ($operator or $cmd->{repeatable}) { +                $last->{cmd} = $cmd; +                $last->{char} = $char; +                $last->{numeric_prefix} = $numeric_prefix; +                $last->{operator} = $operator; +                $last->{movement} = $movement; +                $last->{register} = $register;              } +        } -        # Enter key sends the current input line in command mode as well. -        } elsif ($key == 10) { -            _stop(); -            _commit_line(_input()); +        # Reset the count unless we go into insert mode, _update_mode() needs +        # to know it when leaving insert mode to support insert with counts +        # (like 3i). +        if ($repeat or $cmd->{type} != C_INSERT) { +            $numeric_prefix = undef; +        } +        $operator = undef; +        $movement = undef; + +        if ($cmd != $commands->{'"'} and $register ne '"') { +            print 'Changing register to "' if DEBUG; +            $register = '"';          } -        Irssi::statusbar_items_redraw("vim_mode");      } +    return 1; # call _stop() +} + +sub handle_command_ex { +    my ($key) = @_; + +    # DEL key - remove last character +    if ($key == 127) { +        print "Delete" if DEBUG; +        pop @ex_buf; +        _set_prompt(':' . join '', @ex_buf); + +    # Return key - execute command +    } elsif ($key == 10) { +        print "Run ex-mode command" if DEBUG; +        cmd_ex_command(); +        _update_mode(M_CMD); + +    # Append entered key +    } else { +        push @ex_buf, chr $key; +        _set_prompt(':' . join '', @ex_buf); +    } + +    Irssi::statusbar_items_redraw("vim_windows"); +      _stop();  } +  sub vim_mode_init {      Irssi::signal_add_first 'gui key pressed' => \&got_key;      Irssi::signal_add 'setup changed' => \&setup_changed;      Irssi::statusbar_item_register ('vim_mode', 0, 'vim_mode_cb'); +    Irssi::statusbar_item_register ('vim_windows', 0, 'b_windows_cb');      Irssi::settings_add_str('vim_mode', 'vim_mode_cmd_seq', '');      Irssi::settings_add_bool('vim_mode', 'vim_mode_debug', 0);      Irssi::settings_add_bool('vim_mode', 'vim_mode_utf8', 1); +    Irssi::settings_add_int('vim_mode', 'vim_mode_max_undo_lines', 50);      setup_changed(); +    _reset_undo_buffer();  }  sub setup_changed {      my $value; -    # TODO: okay for now, will cause problems when we have more imaps -    $imaps = {}; +    # Delete all possible imaps created by /set vim_mode_cmd_seq. +    foreach my $char ('a' .. 'z') { +        delete $imaps->{$char}; +    }      $value = Irssi::settings_get_str('vim_mode_cmd_seq');      if ($value) { @@ -1061,7 +2099,7 @@ sub setup_changed {                                   'func' => sub { _update_mode(M_CMD) }                                 };          } else { -            print "Error: vim_mode_cmd_seq must be a single character"; +            _warn("Error: vim_mode_cmd_seq must be a single character");          }      } @@ -1075,74 +2113,133 @@ sub setup_changed {          $non_word = qr/[^\w_\s]/o;      } +    if ($new_utf8 and (!$^V or $^V lt v5.8.1)) { +        _warn("Warning: UTF-8 isn't supported very well in perl < 5.8.1! " . +              "Please disable the vim_mode_utf8 setting."); +    } +      $utf8 = $new_utf8;  }  sub UNLOAD {      Irssi::signal_remove('gui key pressed' => \&got_key);      Irssi::statusbar_item_unregister ('vim_mode'); +    Irssi::statusbar_item_unregister ('vim_windows');  }  sub _add_undo_entry {      my ($line, $pos) = @_; -    # add to the front of the buffer -    print "adding $line to undo list" if DEBUG; -    unshift @undo_buffer, [$line, $pos]; -    $undo_index = 0; + +    # If we aren't at the top of the history stack, then drop newer entries as +    # we can't branch (yet). +    while ($undo_index > 0) { +        shift @undo_buffer; +        $undo_index--; +    } + +    # check it's not a dupe of the list head +    my $current = $undo_buffer[$undo_index]; +    if ($line eq $current->[0] && $pos == $current->[1]) { +        print "Not adding duplicate to undo list" if DEBUG; +    } elsif ($line eq $current->[0]) { +        print "Updating position of undo list at $undo_index" if DEBUG; +        $undo_buffer[$undo_index]->[1] = $pos; +    } else { +        print "adding $line ($pos) to undo list" if DEBUG; +        # add to the front of the buffer +        unshift @undo_buffer, [$line, $pos]; +        $undo_index = 0; +    } +    my $max = Irssi::settings_get_int('vim_mode_max_undo_lines');  }  sub _restore_undo_entry {      my $entry = $undo_buffer[$undo_index]; -    _input($entry->[0], 1); +    _input($entry->[0]);      _input_pos($entry->[1]);  } -sub _clear_undo_buffer { -    print "Clearing undo buffer" if DEBUG; -    @undo_buffer = (['', 0]); -    $undo_index = 0; -} +sub _print_undo_buffer { +    my $i = 0; +    my @buf; +    foreach my $entry (@undo_buffer) { +        my $str = ''; +        if ($i == $undo_index) { +            $str .= '* '; +        } else { +            $str .= '  '; +        } +        my ($line, $pos) = @$entry; +        substr($line, $pos, 0) = '*'; +        # substr($line, $pos+3, 0) = '%_'; -sub _commit_line { -    my ($line) = @_; +        $str .= sprintf('%02d %s [%d]', $i, $line, $pos); +        push @buf, $str; +        $i++; +    } +    print "------ undo buffer ------"; +    print join("\n", @buf); +    print "------------------ ------"; -    my $cmdchars = Irssi::settings_get_str('cmdchars'); +} -    _input(''); -    _update_mode(M_INS); -    _clear_undo_buffer(); +sub _reset_undo_buffer { +    my ($line, $pos) = @_; +    $line = _input()     unless defined $line; +    $pos  = _input_pos() unless defined $pos; + +    print "Clearing undo buffer" if DEBUG; +    @undo_buffer = ([$line, $pos]); +    $undo_index  = 0; +} -    return unless length $line; # ignore empty lines -    my $server = Irssi::active_server(); -    my $win = Irssi::active_win(); -    my $witem = ref $win ? $win->{active} : undef; +sub add_map { +    my ($keys, $command) = @_; -    my @context; -    push @context, $server if defined $server; -    push @context, $witem  if defined $witem; +    # To allow multiple mappings starting with the same key (like gg, ge, gE) +    # also create maps for the keys "leading" to this key (g in this case, but +    # can be longer for this like ,ls). When looking for the mapping these +    # "leading" maps are followed. +    my $tmp = $keys; +    while (length $tmp > 1) { +        my $map = substr $tmp, -1, 1, ''; +        if (not exists $maps->{$tmp}) { +            $maps->{$tmp} = { cmd => undef, maps => {} }; +        } +        if (not exists $maps->{$tmp}->{maps}->{$tmp . $map}) { +            $maps->{$tmp}->{maps}->{$tmp . $map} = undef; +        } +    } -    if ($line =~ /^[\Q$cmdchars\E]/) { -        Irssi::signal_emit 'send command', $line, @context; +    if (not exists $maps->{$keys}) { +        $maps->{$keys} = { char => $keys, +                            cmd => $command, +                           maps => {} +                         };      } else { -        Irssi::signal_emit 'send text', $line, @context; +        $maps->{$keys}->{cmd} = $command;      }  } + +sub _commit_line { +    _update_mode(M_INS); +    _reset_undo_buffer('', 0); +} +  sub _input { -    my ($data, $ignore) = @_; +    my ($data) = @_;      my $current_data = Irssi::parse_special('$L', 0, 0); +      if ($utf8) {          $current_data = decode_utf8($current_data);      }      if (defined $data) { -        if (!$ignore && ($data ne $current_data)) { -            _add_undo_entry($current_data, _input_pos()); -        }          if ($utf8) {              Irssi::gui_input_set(encode_utf8($data));          } else { @@ -1162,11 +2259,19 @@ sub _input_len {  sub _input_pos {      my ($pos) = @_;      my $cur_pos = Irssi::gui_input_get_pos(); +    # my $dpos = defined $pos?$pos:'undef'; +    # my @call = caller(1); +    # my $cfunc = $call[3]; +    # $cfunc =~ s/^.*?::([^:]+)$/$1/; +    # print "pos called from line: $call[2] sub: $cfunc pos: $dpos, cur_pos: $cur_pos" +    #   if DEBUG;      if (defined $pos) { +        #print "Input pos being set from $cur_pos to $pos" if DEBUG;          Irssi::gui_input_set_pos($pos) if $pos != $cur_pos;      } else {          $pos = $cur_pos; +        #print "Input pos retrieved as $pos" if DEBUG;      }      return $pos; @@ -1187,10 +2292,43 @@ sub _stop() {  sub _update_mode {      my ($new_mode) = @_; + +    my $pos; + +    if ($mode == M_INS and $new_mode == M_CMD) { +        # Support counts with insert modes, like 3i. +        if ($numeric_prefix and $numeric_prefix > 1) { +            $pos = _insert_buffer($numeric_prefix - 1, _input_pos()); +            _input_pos($pos); +            $numeric_prefix = undef; + +        # In insert mode we are "between" characters, in command mode "on top" +        # of keys. When leaving insert mode we have to move on key left to +        # accomplish that. +        } else { +            $pos = _input_pos(); +            if ($pos != 0) { +                _input_pos($pos - 1); +            } +        } +        # Store current line to allow undo of i/a/I/A. +        _add_undo_entry(_input(), _input_pos()); + +    # Change mode to i to support insert mode repetition. This doesn't affect +    # commands like i/a/I/A because handle_command_cmd() sets $last->{cmd}. +    # It's necessary when pressing enter so the next line can be repeated. +    } elsif ($mode == M_CMD and $new_mode == M_INS) { +        $last->{cmd} = $commands->{i}; +    # Make sure prompt is cleared when leaving ex mode. +    } elsif ($mode == M_EX and $new_mode != M_EX) { +        _set_prompt(''); +    } +      $mode = $new_mode;      if ($mode == M_INS) {          $history_index = undef;          $register = '"'; +        @insert_buf = ();      # Reset every command mode related status as a fallback in case something      # goes wrong.      } elsif ($mode == M_CMD) { @@ -1198,6 +2336,11 @@ sub _update_mode {          $operator = undef;          $movement = undef;          $register = '"'; + +        $pending_map = undef; + +        # Also clear ex-mode buffer. +        @ex_buf = ();      }      Irssi::statusbar_items_redraw("vim_mode"); @@ -1210,5 +2353,8 @@ sub _set_prompt {      Irssi::signal_emit('change prompt', $msg);  } -# TODO: -# 10gg -> go to window 10 (prefix.gg -> win <prefix>) +sub _warn { +    my ($warning) = @_; + +    print '%_vim_mode: ', $warning, '%_'; +} | 
