diff options
| -rw-r--r-- | vim-mode/vim_mode.pl | 255 | 
1 files changed, 157 insertions, 98 deletions
| diff --git a/vim-mode/vim_mode.pl b/vim-mode/vim_mode.pl index 5e33bf4..28e254d 100644 --- a/vim-mode/vim_mode.pl +++ b/vim-mode/vim_mode.pl @@ -44,7 +44,7 @@  #   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 +# * Scroll the scrollback buffer: Ctrl-E Ctrl-D Ctrl-Y 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 @@ -69,19 +69,27 @@  # * Display windows:   :ls :buffers  # * Display registers: :reg[isters] :di[splay] {args}  # * Display undolist:  :undol[ist] (mostly used for debugging) +# * Source files       :so[urce] - only sources vim_moderc at the moment, +#                                  {file} not supported  # * Mappings:          :map             - display custom mappings  #                      :map {lhs} {rhs} - add mapping +# * Substitute:        :s/// - i and g are supported as flags, only /// can be +#                              used as separator, uses Perl regex instead of +#                              Vim regex +#  #  # Mappings:  #  # {lhs} is the key combination to be mapped, {rhs} the target. The <> notation  # is used (e.g. <C-F> is Ctrl-F), case is ignored. Supported <> keys: -# <C-A>-<C-Z>, <C-^>, <C-6>, <Space>, <CR>. Mapping ex-mode command is -# supported. Only default mappings can be used in {rhs}. +# <C-A>-<C-Z>, <C-^>, <C-6>, <Space>, <CR>. Mapping ex-mode and irssi commands +# is supported. Only default mappings can be used in {rhs}.  # Examples: +#     :map w  W      # to remap w to work like W  #     :map gb :bnext # to map gb to call :bnext  #     :map gB :bprev -#     :map w  W      # to remap w to work like W +#     :map <C-L> /clear # map Ctrl-L to irssi command /clear +#     :map <C-G> /window goto 1  #  #  # The following irssi settings are available: @@ -96,6 +104,19 @@  # * vim_windows: displays windows selected with :b  #  # +# Configuration +# +# Additionally to the irssi settings described above vim_mode can be +# configured through an external configuration file named "vim_moderc" located +# in ~/.irssi/vim_moderc. If available it's loaded on startup and every +# supported ex-command is run. It's syntax is similar to "vimrc". To (re)load +# it while vim_mode is running use :so[urce]. +# +# Supported ex-commands: +# +# * :map +# +#  # Installation:  #  # As always copy the script into .irssi/scripts and load it with @@ -135,6 +156,7 @@  # Known bugs:  #  # * count before register doesn't work: e.g. 3"ap doesn't work, but "a3p does +# * undo/redo positions are mostly wrong  #  #  # TODO: @@ -200,22 +222,27 @@ $VERSION = "1.0.1";  # CONSTANTS -sub M_CMD() { 1 } # command mode -sub M_INS() { 0 } # insert mode -sub M_EX () { 2 } # extended mode (after a :?) +# command mode +sub M_CMD () { 1 } +# insert mode +sub M_INS () { 0 } +# extended mode (after a :?) +sub M_EX () { 2 }  # operator command  sub C_OPERATOR () { 0 } -# normal commmand +# normal command, no special handling necessary  sub C_NORMAL () { 1 }  # command taking another key as argument  sub C_NEEDSKEY () { 2 } -# text-object commmand (i a) +# text-object command (i a)  sub C_TEXTOBJECT () { 3 }  # commands entering insert mode  sub C_INSERT () { 4 }  # ex-mode commands  sub C_EX () { 5 } +# irssi commands +sub C_IRSSI () { 6 }  # word and non-word regex, keep in sync with setup_changed()!  my $word     = qr/[\w_]/o; @@ -246,8 +273,10 @@ my $commands       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, +              selection_needs_move_right => 1 }, +      t  => { char => 't', func => \&cmd_t, type => C_NEEDSKEY, +              selection_needs_move_right => 1 },        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 }, @@ -255,12 +284,16 @@ my $commands       # 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 }, +     e  => { char => 'e',  func => \&cmd_e,  type => C_NORMAL, +             selection_needs_move_right => 1 }, +     ge => { char => 'ge', func => \&cmd_ge, type => C_NORMAL, +             selection_needs_move_right => 1 },       W  => { char => 'W',  func => \&cmd_W,  type => C_NORMAL }, -     B  => { char => 'B',  func => \&cmd_B,  type => C_NORMAL }, +     B  => { char => 'B',  func => \&cmd_B,  type => C_NORMAL, +             selection_needs_move_right => 1 },       E  => { char => 'E',  func => \&cmd_E,  type => C_NORMAL }, -     gE => { char => 'gE', func => \&cmd_gE, type => C_NORMAL }, +     gE => { char => 'gE', func => \&cmd_gE, type => C_NORMAL, +             selection_needs_move_right => 1 },       # 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 }, @@ -288,7 +321,7 @@ my $commands              repeatable => 1 },       # replace       r => { char => 'r', func => \&cmd_r, type => C_NEEDSKEY, -              repeatable => 1 }, +            repeatable => 1 },       # paste       p => { char => 'p', func => \&cmd_p, type => C_NORMAL,              repeatable => 1 }, @@ -300,14 +333,14 @@ my $commands       D => { char => 'D', func => \&cmd_D, type => C_NORMAL,              repeatable => 1 },       # scrolling +     "\x05" => { char => '<C-E>', func => \&cmd_ctrl_d, type => C_NORMAL },       "\x04" => { char => '<C-D>', func => \&cmd_ctrl_d, type => C_NORMAL, -                 repeatable => 1 }, # half screen down +                 needs_count => 1 }, +     "\x19" => { char => '<C-Y>', func => \&cmd_ctrl_u, type => C_NORMAL },       "\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 +                 needs_count => 1 }, +     "\x06" => { char => '<C-F>', func => \&cmd_ctrl_f, type => C_NORMAL }, +     "\x02" => { char => '<C-B>', func => \&cmd_ctrl_b, type => C_NORMAL },       # 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 }, @@ -327,24 +360,26 @@ my $commands  # All available commands in Ex-Mode.  my $commands_ex    = { -     s         => { char => 's',         func => \&ex_substitute, type => C_EX }, -     bnext     => { char => 'bnext',     func => \&ex_bnext,      type => C_EX }, -     bn        => { char => 'bn',        func => \&ex_bnext,      type => C_EX }, -     bprev     => { char => 'bprev',     func => \&ex_bprev,      type => C_EX }, -     bp        => { char => 'bp',        func => \&ex_bprev,      type => C_EX }, -     bdelete   => { char => 'bdelete',   func => \&ex_bdelete,    type => C_EX }, -     bd        => { char => 'bd',        func => \&ex_bdelete,    type => C_EX }, -     buffer    => { char => 'buffer',    func => \&ex_buffer,     type => C_EX }, -     b         => { char => 'b',         func => \&ex_buffer,     type => C_EX }, -     registers => { char => 'registers', func => \&ex_registers,  type => C_EX }, -     reg       => { char => 'reg',       func => \&ex_registers,  type => C_EX }, -     display   => { char => 'display',   func => \&ex_registers,  type => C_EX }, -     di        => { char => 'di',        func => \&ex_registers,  type => C_EX }, -     buffers   => { char => 'buffer',    func => \&ex_buffers,    type => C_EX }, -     ls        => { char => 'ls',        func => \&ex_buffers,    type => C_EX }, -     undolist  => { char => 'undolist',  func => \&ex_undolist,   type => C_EX }, -     undol     => { char => 'undol',     func => \&ex_undolist,   type => C_EX }, -     map       => { char => 'map',       func => \&ex_map,        type => C_EX }, +     s         => { char => ':s',         func => \&ex_substitute, type => C_EX }, +     bnext     => { char => ':bnext',     func => \&ex_bnext,      type => C_EX }, +     bn        => { char => ':bn',        func => \&ex_bnext,      type => C_EX }, +     bprev     => { char => ':bprev',     func => \&ex_bprev,      type => C_EX }, +     bp        => { char => ':bp',        func => \&ex_bprev,      type => C_EX }, +     bdelete   => { char => ':bdelete',   func => \&ex_bdelete,    type => C_EX }, +     bd        => { char => ':bd',        func => \&ex_bdelete,    type => C_EX }, +     buffer    => { char => ':buffer',    func => \&ex_buffer,     type => C_EX }, +     b         => { char => ':b',         func => \&ex_buffer,     type => C_EX }, +     registers => { char => ':registers', func => \&ex_registers,  type => C_EX }, +     reg       => { char => ':reg',       func => \&ex_registers,  type => C_EX }, +     display   => { char => ':display',   func => \&ex_registers,  type => C_EX }, +     di        => { char => ':di',        func => \&ex_registers,  type => C_EX }, +     buffers   => { char => ':buffer',    func => \&ex_buffers,    type => C_EX }, +     ls        => { char => ':ls',        func => \&ex_buffers,    type => C_EX }, +     undolist  => { char => ':undolist',  func => \&ex_undolist,   type => C_EX }, +     undol     => { char => ':undol',     func => \&ex_undolist,   type => C_EX }, +     map       => { char => ':map',       func => \&ex_map,        type => C_EX }, +     source    => { char => ':source',    func => \&ex_source,     type => C_EX }, +     so        => { char => ':so',        func => \&ex_source,     type => C_EX },      };  # MAPPINGS @@ -433,7 +468,7 @@ my $imap = undef;  # maps for insert mode  my $imaps    = { -     # ctrl-r, insert register +     # CTRL-R, insert register       "\x12" => { map  => undef, func => \&insert_ctrl_r },      }; @@ -477,11 +512,12 @@ sub insert_ctrl_r {  # COMMAND MODE OPERATORS  sub cmd_operator_c { -    my ($old_pos, $new_pos, $move, $repeat) = @_; +    my ($old_pos, $new_pos, $move_cmd, $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') { +    if ($move_cmd and ($move_cmd == $commands->{w} or +                       $move_cmd == $commands->{W})) {          my $input = _input();          if ($new_pos - $old_pos > 1 and                  substr($input, $new_pos - 1, 1) =~ /\s/) { @@ -489,7 +525,7 @@ sub cmd_operator_c {          }      } -    cmd_operator_d($old_pos, $new_pos, $move, $repeat, 1); +    cmd_operator_d($old_pos, $new_pos, $move_cmd, $repeat, 1);      if (!$repeat) {          _update_mode(M_INS); @@ -500,9 +536,9 @@ sub cmd_operator_c {      }  }  sub cmd_operator_d { -    my ($old_pos, $new_pos, $move, $repeat, $change) = @_; +    my ($old_pos, $new_pos, $move_cmd, $repeat, $change) = @_; -    my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move); +    my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move_cmd);      # Remove the selected string from the input.      my $input = _input(); @@ -523,15 +559,9 @@ sub cmd_operator_d {      _input_pos($pos);  }  sub cmd_operator_y { -    my ($old_pos, $new_pos, $move, $repeat) = @_; +    my ($old_pos, $new_pos, $move_cmd, $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--; -    } +    my ($pos, $length) = _get_pos_and_length($old_pos, $new_pos, $move_cmd);      # Extract the selected string and put it in the " register.      my $input = _input(); @@ -556,7 +586,7 @@ sub cmd_operator_y {      }  }  sub _get_pos_and_length { -    my ($old_pos, $new_pos, $move) = @_; +    my ($old_pos, $new_pos, $move_cmd) = @_;      my $length = $new_pos - $old_pos;      # We need a positive length and $old_pos must be smaller. @@ -565,16 +595,9 @@ sub _get_pos_and_length {          $length *= -1;      } -    # 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') { +    # Some commands don't move one character after the deletion area which is +    # necessary for all commands moving to the right. Fix it. +    if ($move_cmd->{selection_needs_move_right}) {          $length += 1;      } @@ -1127,7 +1150,7 @@ sub cmd_dollar {  sub cmd_x {      my ($count, $pos, $repeat) = @_; -    cmd_operator_d($pos, $pos + $count, 'x'); +    cmd_operator_d($pos, $pos + $count, $commands->{x}, $repeat);      return (undef, undef);  }  sub cmd_X { @@ -1137,7 +1160,7 @@ sub cmd_X {      my $new = $pos - $count;      $new = 0 if $new < 0; -    cmd_operator_d($pos, $new, 'X'); +    cmd_operator_d($pos, $new, $commands->{X}, $repeat);      return (undef, undef);  }  sub cmd_s { @@ -1427,6 +1450,7 @@ sub cmd_ex_command {  sub ex_substitute {      my ($arg_str) = @_; +    # :s///      if ($arg_str =~ m|^s/(.+)/(.*)/([ig]*)|) {          my ($search, $replace, $flags) = ($1, $2, $3);          print "Searching for $search, replace: $replace, flags; $flags" @@ -1545,7 +1569,7 @@ sub ex_map {      my ($arg_str) = @_;      # :map {lhs} {rhs} -    if ($arg_str =~ /^map (\S+) (\S+)$/) { +    if ($arg_str =~ /^map (\S+) (\S.+)$/) {          my $lhs = _parse_mapping($1);          my $rhs = $2; @@ -1555,6 +1579,7 @@ sub ex_map {          # Add new mapping.          my $command; +        # Ex-mode command          if (index($rhs, ':') == 0) {              $rhs = substr $rhs, 1;              if (not exists $commands_ex->{$rhs}) { @@ -1562,6 +1587,13 @@ sub ex_map {              } else {                  $command = $commands_ex->{$rhs};              } +        # Irssi command +        } elsif (index($rhs, '/') == 0) { +            $command = { char => $rhs, +                         func => substr($rhs, 1), +                         type => C_IRSSI, +                       }; +        # command-mode command          } else {              $rhs = _parse_mapping($2);              if (not defined $rhs) { @@ -1580,15 +1612,10 @@ sub ex_map {          foreach my $key (sort keys %$maps) {              my $map = $maps->{$key};              my $cmd = $map->{cmd}; -            if (defined $map->{char}) { -                my $char = _parse_mapping_reverse($map->{char}); -                next if $char eq $cmd->{char}; # skip default mappings - -                my $cmdc = _parse_mapping_reverse($cmd->{char}); -                if ($cmd->{type} == C_EX) { -                    $cmdc = ":$cmdc"; -                } -                $active_window->print(sprintf "%-15s %s", $char, $cmdc); +            if (defined $cmd) { +                next if $map->{char} eq $cmd->{char}; # skip default mappings +                $active_window->print(sprintf "%-15s %s", $map->{char}, +                                                          $cmd->{char});              }          }      } else { @@ -1641,6 +1668,24 @@ sub _parse_mapping_reverse {      return $string;  } +sub ex_source { +    # :so[urce], but only loads the vim_moderc file at the moment + +    open my $file, '<', Irssi::get_irssi_dir() . '/vim_moderc' or return; + +    while (my $line = <$file>) { +        next if $line =~ /^\s*$/ or $line =~ /^\s*"/; + +        chomp $line; +        # :map {lhs} {rhs}, keep in sync with ex_map() +        if ($line =~ /^\s*map (\S+) (\S.+)$/) { +            ex_map($line); +        } else { +            _warn_ex('source', "command not supported: $line"); +        } +    } +} +  sub _warn_ex {      my ($command, $description) = @_;      my $message = "Error in ex-mode command $command"; @@ -1814,7 +1859,7 @@ sub got_key {          } elsif ($key == 127) {              @insert_buf = ();          # All other entered characters need to be stored to allow repeat of -        # insert mode. Ignore delete and ctrl characters. +        # insert mode. Ignore delete and control characters.          } elsif ($key > 31) {              push @insert_buf, chr($key);          } @@ -1984,10 +2029,16 @@ sub handle_command_cmd {      }      # Ex-mode commands can also be bound in command mode. Works only if the -    # ex-mode command doesn't take any arguments. +    # ex-mode command doesn't need any additional arguments.      if ($cmd->{type} == C_EX) { -        $cmd->{func}->(); +        print "Processing ex-command: $map->{char} ($cmd->{char})" if DEBUG; +        $cmd->{func}->($cmd->{char});          return 1; # call _stop() +    # As can irssi commands. +    } elsif ($cmd->{type} == C_IRSSI) { +        print "Processing irssi-command: $map->{char} ($cmd->{char})" if DEBUG; +        Irssi::command($cmd->{func}); +        return 1; # call _stop();      }      # text-objects (i a) are simulated with $movement @@ -2011,7 +2062,7 @@ sub handle_command_cmd {                  print "Processing line operator: $map->{char} ($cmd->{char})"                      if DEBUG;                  my $pos = _input_pos(); -                $cmd->{func}->(0, _input_len(), '', 0); +                $cmd->{func}->(0, _input_len(), undef, 0);                  # Restore position for yy.                  if ($cmd == $commands->{y}) {                      _input_pos($pos); @@ -2044,7 +2095,7 @@ sub handle_command_cmd {          _commit_line();          return 0; # don't call _stop() -    } else { #if ($movement || exists $movements->{$char}) { +    } else {          print "Processing command: $map->{char} ($cmd->{char})" if DEBUG;          my $skip = 0; @@ -2118,20 +2169,14 @@ sub handle_command_cmd {              # 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); +                $operator->{func}->($cur_pos, $new_pos, $cmd, $repeat);              }              # 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 +                # TODO: why do history entries still show up in undo                  # buffer? Is avoiding the commands here insufficient?                  _add_undo_entry(_input(), _input_pos()); @@ -2173,8 +2218,13 @@ sub handle_command_ex {      # DEL key - remove last character      if ($key == 127) {          print "Delete" if DEBUG; -        pop @ex_buf; -        _set_prompt(':' . join '', @ex_buf); +        if (scalar @ex_buf > 0) { +            pop @ex_buf; +            _set_prompt(':' . join '', @ex_buf); +        # Backspacing over : exits ex-mode. +        } else { +            _update_mode(M_CMD); +        }      # Return key - execute command      } elsif ($key == 10) { @@ -2182,6 +2232,10 @@ sub handle_command_ex {          cmd_ex_command();          _update_mode(M_CMD); +    # Ignore control characters for now. +    } elsif ($key < 32) { +        # TODO: use them later, e.g. completion +      # Append entered key      } else {          push @ex_buf, chr $key; @@ -2205,6 +2259,9 @@ sub vim_mode_init {      Irssi::settings_add_bool('vim_mode', 'vim_mode_utf8', 1);      Irssi::settings_add_int('vim_mode', 'vim_mode_max_undo_lines', 50); +    # Load the vim_moderc file if it exists. +    ex_source('source'); +      setup_changed();      _reset_undo_buffer();  } @@ -2332,7 +2389,10 @@ sub add_map {      while (length $tmp > 1) {          my $map = substr $tmp, -1, 1, '';          if (not exists $maps->{$tmp}) { -            $maps->{$tmp} = { cmd => undef, maps => {} }; +            $maps->{$tmp} = { char => _parse_mapping_reverse($tmp), +                               cmd => undef, +                              maps => {} +                            };          }          if (not exists $maps->{$tmp}->{maps}->{$tmp . $map}) {              $maps->{$tmp}->{maps}->{$tmp . $map} = undef; @@ -2340,14 +2400,13 @@ sub add_map {      }      if (not exists $maps->{$keys}) { -        $maps->{$keys} = { char => $keys, -                            cmd => $command, +        $maps->{$keys} = { char => undef, +                            cmd => undef,                             maps => {}                           }; -    } else { -        $maps->{$keys}->{char} = $keys; -        $maps->{$keys}->{cmd} = $command;      } +    $maps->{$keys}->{char} = _parse_mapping_reverse($keys); +    $maps->{$keys}->{cmd} = $command;  } | 
