aboutsummaryrefslogtreecommitdiffstats
path: root/vim-mode
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--vim-mode/vim_mode.pl2098
1 files changed, 1622 insertions, 476 deletions
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, '%_';
+}