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, '%_'; +} |