From bb8a56ba284990ed02909fc025090f2804197c59 Mon Sep 17 00:00:00 2001 From: Tom Feist Date: Thu, 5 May 2011 00:59:27 +0100 Subject: vim-mode: new branch for fixing input code. Move it all to single location (end of file), and rename got_key handler to something standard (sig_gui_keypress) --- vim-mode/vim_mode.pl | 1684 +++++++++++++++++++++++++------------------------- 1 file changed, 846 insertions(+), 838 deletions(-) (limited to 'vim-mode') diff --git a/vim-mode/vim_mode.pl b/vim-mode/vim_mode.pl index ee8ba2a..26887b7 100644 --- a/vim-mode/vim_mode.pl +++ b/vim-mode/vim_mode.pl @@ -2694,1068 +2694,1076 @@ sub b_windows_cb { } -# INPUT HANDLING -sub got_key { - my ($key) = @_; +sub _tab_complete { + my ($input, $source) = @_; + my @out; + foreach my $item (@$source) { + if ($item =~ m/^\Q$input\E/) { + push @out, $item; + } + } - return if ($should_ignore); + return sort { $a cmp $b } @out; +} - # Esc key - if ($key == 27) { - print "Esc seen, starting buffer" if DEBUG; - $input_buf_enabled = 1; +sub vim_mode_init { + Irssi::signal_add_first 'gui key pressed' => \&sig_gui_keypress; + Irssi::statusbar_item_register ('vim_mode', 0, 'vim_mode_cb'); + Irssi::statusbar_item_register ('vim_windows', 0, 'b_windows_cb'); - # NOTE: this timeout might be too low on laggy systems, but - # it comes at the cost of keystroke latency for things that - # contain escape sequences (arrow keys, etc) - my $esc_buf_timeout = $settings->{esc_buf_timeout}->{value}; + Irssi::expando_create('vim_cmd_mode' => \&vim_exp_mode, {}); + Irssi::expando_create('vim_wins' => \&vim_exp_wins, {}); - $input_buf_timer - = Irssi::timeout_add_once($esc_buf_timeout, - \&handle_input_buffer, undef); - print "Buffer Timer tag: $input_buf_timer" if DEBUG; + # Register all available settings. + foreach my $name (keys %$settings) { + _setting_register($name); + } - } elsif ($mode == M_INS) { + foreach my $char ('a' .. 'z') { + $registers->{$char} = ''; + } - if ($key == 3) { # Ctrl-C enters command mode - _update_mode(M_CMD); - _stop(); - return; + setup_changed(); - } elsif ($key == 10) { # enter. - _commit_line(); + Irssi::signal_add 'setup changed' => \&setup_changed; - } elsif ($input_buf_enabled and $imap) { - print "Imap $imap active" if DEBUG; - my $map = $imaps->{$imap}; - 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 { - push @input_buf, $key; - } - flush_input_buffer(); - _stop(); - $imap = undef; - return; + # Add all default mappings. + foreach my $char (keys %$commands) { + next if $char =~ /^_/; # skip private commands (text-objects for now) + add_map($char, $commands->{$char}); + } - } elsif (exists $imaps->{chr($key)}) { - print "Imap " . chr($key) . " seen, starting buffer" if DEBUG; + # Load the vim_moderc file if it exists. + ex_source('source'); - # start imap pending mode - $imap = chr($key); + setup_changed(); + _reset_undo_buffer(); - $input_buf_enabled = 1; - push @input_buf, $key; - $input_buf_timer - = Irssi::timeout_add_once(1000, \&flush_input_buffer, undef); + if ($settings->{start_cmd}->{value}) { + _update_mode(M_CMD); + } else { + _update_mode(M_INS); + } +} - _stop(); - return; +sub setup_changed { + my $value; - # Pressing delete resets insert mode repetition (8 = BS, 127 = DEL). - # TODO: maybe allow it - } elsif ($key == 8 || $key == 127) { - @insert_buf = (); - # All other entered characters need to be stored to allow repeat of - # insert mode. Ignore delete and control characters. - } elsif ($key > 31) { - push @insert_buf, chr($key); - } + if ($settings->{cmd_seq}->{value} ne '') { + delete $imaps->{$settings->{cmd_seq}->{value}}; } - - if ($input_buf_enabled) { - push @input_buf, $key; - _stop(); - return; + $value = _setting_get('cmd_seq'); + if ($value eq '') { + $settings->{cmd_seq}->{value} = $value; + } else { + if (length $value == 1) { + $imaps->{$value} = { 'map' => $value, + 'func' => sub { _update_mode(M_CMD) } + }; + $settings->{cmd_seq}->{value} = $value; + } else { + _warn("Error: vim_mode_cmd_seq must be a single character"); + # Restore the value so $settings and irssi settings are + # consistent. + _setting_set('cmd_seq', $settings->{cmd_seq}->{value}); + } } - if ($mode == M_CMD) { - my $should_stop = handle_command_cmd($key); - _stop() if $should_stop; - Irssi::statusbar_items_redraw("vim_mode"); + my $new_utf8 = _setting_get('utf8'); + if ($new_utf8 != $settings->{utf8}->{value}) { + # recompile the patterns when switching to/from utf-8 + $word = qr/[\w_]/o; + $non_word = qr/[^\w_\s]/o; - } elsif ($mode == M_EX) { + $settings->{utf8}->{value} = $new_utf8; + } + 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."); + } - if ($key == 3) { # C-c cancels Ex mdoe as well. - _update_mode(M_CMD); - _stop(); - return; - } + # Sync $settings with current irssi values. + foreach my $name (keys %$settings) { + # These were already handled above. + next if $name eq 'cmd_seq' or $name eq 'cmd_seq'; - handle_command_ex($key); + $settings->{$name}->{value} = _setting_get($name); } } -# TODO: merge this with 'flush_input_buffer' below. - -sub handle_input_buffer { - - #Irssi::timeout_remove($input_buf_timer); - $input_buf_timer = undef; - # see what we've collected. - print "Input buffer contains: ", join(", ", @input_buf) if DEBUG; +sub UNLOAD { + Irssi::signal_remove('gui key pressed' => \&got_key); + Irssi::signal_remove('setup changed' => \&setup_changed); + Irssi::statusbar_item_unregister ('vim_mode'); + Irssi::statusbar_item_unregister ('vim_windows'); +} - if (@input_buf == 1 && $input_buf[0] == 27) { +sub _add_undo_entry { + my ($line, $pos) = @_; - print "Enter Command Mode" if DEBUG; - _update_mode(M_CMD); + # 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 { - # we have more than a single esc, implying an escape sequence - # (meta-* or esc-*) + 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 = $settings->{max_undo_lines}->{value}; +} - # currently, we only extract escape sequences if: - # a) we're in ex mode - # b) they're arrow keys (for history control) +sub _restore_undo_entry { + my $entry = $undo_buffer[$undo_index]; + _input($entry->[0]); + _input_pos($entry->[1]); +} - if ($mode == M_EX) { - # ex mode - my $key_str = join '', map { chr } @input_buf; - if ($key_str =~ m/^\e\[([ABCD])/) { - my $arrow = $1; - _debug( "Arrow key: $arrow"); - if ($arrow eq 'A') { # up - ex_history_back(); - } elsif ($arrow eq 'B') { # down - ex_history_fwd(); - } else { - $arrow =~ s/C/right/; - $arrow =~ s/D/left/; - _debug("Arrow key $arrow not supported"); - } - } +sub _print_undo_buffer { + + my $i = 0; + my @buf; + foreach my $entry (@undo_buffer) { + my $str = ''; + if ($i == $undo_index) { + $str .= '* '; } else { - # otherwise, we just forward them to irssi. - _emulate_keystrokes(@input_buf); + $str .= ' '; } + my ($line, $pos) = @$entry; + substr($line, $pos, 0) = '*'; + # substr($line, $pos+3, 0) = '%_'; - # Clear insert buffer, pressing "special" keys (like arrow keys) - # resets it. - @insert_buf = (); + $str .= sprintf('%02d %s [%d]', $i, $line, $pos); + push @buf, $str; + $i++; } + print "------ undo buffer ------"; + print join("\n", @buf); + print "------------------ ------"; - @input_buf = (); - $input_buf_enabled = 0; } -sub flush_input_buffer { - Irssi::timeout_remove($input_buf_timer) if defined $input_buf_timer; - $input_buf_timer = undef; - # 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 = (); - $input_buf_enabled = 0; +sub _reset_undo_buffer { + my ($line, $pos) = @_; + $line = _input() unless defined $line; + $pos = _input_pos() unless defined $pos; - $imap = undef; + print "Clearing undo buffer" if DEBUG; + @undo_buffer = ([$line, $pos]); + $undo_index = 0; } -sub flush_pending_map { - my ($old_pending_map) = @_; - - print "flush_pending_map(): ", $pending_map, ' ', $old_pending_map - if DEBUG; +sub add_map { + my ($keys, $command) = @_; - return if not defined $pending_map or - $pending_map ne $old_pending_map; + # 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} = { char => _parse_mapping_reverse($tmp), + cmd => undef, + maps => {} + }; + } + if (not exists $maps->{$tmp}->{maps}->{$tmp . $map}) { + $maps->{$tmp}->{maps}->{$tmp . $map} = undef; + } + } - handle_command_cmd(undef); - Irssi::statusbar_items_redraw("vim_mode"); + if (not exists $maps->{$keys}) { + $maps->{$keys} = { char => undef, + cmd => undef, + maps => {} + }; + } + $maps->{$keys}->{char} = _parse_mapping_reverse($keys); + $maps->{$keys}->{cmd} = $command; } -sub handle_numeric_prefix { - my ($char) = @_; - my $num = 0+$char; +sub delete_map { + my ($keys) = @_; - if (defined $numeric_prefix) { - $numeric_prefix *= 10; - $numeric_prefix += $num; - } else { - $numeric_prefix = $num; - } -} + # Abort for non-existent mappings or placeholder mappings. + return if not exists $maps->{$keys} or not defined $maps->{$keys}->{cmd}; -sub handle_command_cmd { - my ($key) = @_; + my @add = (); - my $pending_map_flushed = 0; + # If no maps need the current key, then remove it and all other + # unnecessary keys in the "tree". + if (keys %{$maps->{$keys}->{maps}} == 0) { + my $tmp = $keys; + while (length $tmp > 1) { + my $map = substr $tmp, -1, 1, ''; + delete $maps->{$tmp}->{maps}->{$tmp . $map}; + if (not $maps->{$tmp}->{cmd} and keys %{$maps->{$tmp}->{maps}} == 0) { + push @add, $tmp; + delete $maps->{$tmp}; + } else { + last; + } + } + } - my $char; - if (defined $key) { - $char = chr($key); - # We were called from flush_pending_map(). + if (keys %{$maps->{$keys}->{maps}} > 0) { + $maps->{$keys}->{cmd} = undef; } else { - $char = $pending_map; - $key = 0; - $pending_map_flushed = 1; + delete $maps->{$keys}; } + push @add, $keys; - # Counts - if (!$movement and !$pending_map 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() + # Restore default keybindings in case we :unmapped a or a remapped + # key. + foreach my $key (@add) { + if (exists $commands->{$key}) { + add_map($key, $commands->{$key}); + } } +} - 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 => {}, - }; +sub _commit_line { + _update_mode(M_INS); - } elsif (exists $maps->{$char}) { - $map = $maps->{$char}; + # separate from call above as _update_mode() does additional internal work + # and we need to make sure it gets correctly called. + _update_mode(M_CMD) if $settings->{start_cmd}->{value}; - # 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; - } + _reset_undo_buffer('', 0); +} - # 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() - } +sub _input { + my ($data) = @_; + + my $current_data = Irssi::parse_special('$L', 0, 0); + if ($settings->{utf8}->{value}) { + $current_data = decode_utf8($current_data); + } + + if (defined $data) { + if ($settings->{utf8}->{value}) { + Irssi::gui_input_set(encode_utf8($data)); + } else { + Irssi::gui_input_set($data); + } } else { - print "No mapping found for $char" if DEBUG; - $pending_map = undef; - $numeric_prefix = undef; - return 1; # call _stop() + $data = $current_data; } - $pending_map = undef; + return $data; +} - my $cmd = $map->{cmd}; +sub _input_len { + return length _input(); +} - # 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() +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; } - # Ex-mode commands can also be bound in command mode. - if ($cmd->{type} == C_EX) { - print "Processing ex-command: $map->{char} ($cmd->{char})" if DEBUG; + return $pos; +} - $cmd->{func}->(substr($cmd->{char}, 1), $numeric_prefix); - $numeric_prefix = undef; +sub _emulate_keystrokes { + my @keys = @_; + $should_ignore = 1; + for my $key (@keys) { + Irssi::signal_emit('gui key pressed', $key); + } + $should_ignore = 0; +} - return 1; # call _stop() - # As can irssi commands. - } elsif ($cmd->{type} == C_IRSSI) { - print "Processing irssi-command: $map->{char} ($cmd->{char})" if DEBUG; +sub _stop() { + Irssi::signal_stop_by_name('gui key pressed'); +} - _command_with_context($cmd->{func}); +sub _update_mode { + my ($new_mode) = @_; - $numeric_prefix = undef; - return 1; # call _stop(); - # does nothing. - } elsif ($cmd->{type} == C_NOP) { - print "Processing : $map->{char}" if DEBUG; + my $pos; - $numeric_prefix = undef; - return 1; # call _stop(); - } + 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; - # 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}; + # 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 { - $movement = $cmd; + $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()); - } 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; + # 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(''); + } - my $pos = _input_pos(); - $cmd->{func}->(0, _input_len(), undef, 0); - # Restore position for yy. - if ($cmd == $commands->{y}) { - _input_pos($pos); - # And save undo for other operators. - } else { - _add_undo_entry(_input(), _input_pos()); - } - if ($register ne '"') { - print 'Changing register to "' if DEBUG; - $register = '"'; - } - } - $numeric_prefix = undef; - $operator = undef; - $movement = undef; - # Set new operator. - } else { - $operator = $cmd; - } + $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) { + $numeric_prefix = undef; + $operator = undef; + $movement = undef; + $register = '"'; - # Start Ex mode. - } elsif ($cmd == $commands->{':'}) { + $pending_map = undef; - if (not script_is_loaded('uberprompt')) { - _warn("Warning: Ex mode requires the 'uberprompt' script. " . - "Please load it and try again."); - } else { - _update_mode(M_EX); - _set_prompt(':'); - } + # Also clear ex-mode buffer. + @ex_buf = (); + } - # Enter key sends the current input line in command mode as well. - } elsif ($key == 10) { - _commit_line(); - return 0; # don't call _stop() + Irssi::statusbar_items_redraw("vim_mode"); + Irssi::statusbar_items_redraw ('uberprompt'); - } else { - print "Processing command: $map->{char} ($cmd->{char})" if DEBUG; +} - my $skip = 0; - my $repeat = 0; +sub _set_prompt { + my $msg = shift; - 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; - } - } + # add a leading space unless we're trying to clear it entirely. + if (length($msg) and $settings->{prompt_leading_space}->{value}) { + $msg = ' ' . $msg; + } - # Ignore invalid operator/command combinations. - if ($operator and $cmd->{no_operator}) { - print "Invalid operator/command: $operator->{char} $cmd->{char}" - if DEBUG; - $skip = 1; - } + # escape % symbols. This prevents any _set_prompt calls from using + # colouring sequences. + $msg =~ s/%/%%/g; - 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; - } + Irssi::signal_emit('change prompt', $msg, 'UP_INNER'); +} - my $cur_pos = _input_pos(); +sub _setting_get { + my ($name) = @_; - # 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 { - ($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(); - } + my $type = $settings->{$name}->{type}; + $name = "vim_mode_$name"; - # 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; - } + my $ret = undef; - # 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; - $operator->{func}->($cur_pos, $new_pos, $cmd, $repeat); - } + if ($type == S_BOOL) { + $ret = Irssi::settings_get_bool($name); + } elsif ($type == S_INT) { + $ret = Irssi::settings_get_int($name); + } elsif ($type == S_STR) { + $ret = Irssi::settings_get_str($name); + } else { + _warn("Unknown setting type '$type', please report."); + } - # 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 history entries still show up in undo - # buffer? Is avoiding the commands here insufficient? + return $ret; +} - _add_undo_entry(_input(), _input_pos()); - } +sub _setting_set { + my ($name, $value) = @_; - # 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; - } - } + my $type = $settings->{$name}->{type}; + $name = "vim_mode_$name"; - # 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 ($type == S_BOOL) { + Irssi::settings_set_bool($name, $value); + } elsif ($type == S_INT) { + Irssi::settings_set_int($name, $value); + } elsif ($type == S_STR) { + Irssi::settings_set_str($name, $value); + } else { + _warn("Unknown setting type '$type', please report."); + } +} +sub _setting_register { + my ($name) = @_; - if ($cmd != $commands->{'"'} and $register ne '"') { - print 'Changing register to "' if DEBUG; - $register = '"'; - } + my $value = $settings->{$name}->{value}; + my $type = $settings->{$name}->{type}; + $name = "vim_mode_$name"; + if ($type == S_BOOL) { + Irssi::settings_add_bool('vim_mode', $name, $value); + } elsif ($type == S_INT) { + Irssi::settings_add_int('vim_mode', $name, $value); + } elsif ($type == S_STR) { + Irssi::settings_add_str('vim_mode', $name, $value); + } else { + _warn("Unknown setting type '$type', please report."); } - - return 1; # call _stop() } -sub handle_command_ex { - my ($key) = @_; +sub _warn { + my ($warning) = @_; - # BS key (8) or DEL key (127) - remove last character. - if ($key == 8 || $key == 127) { - print "Delete" if DEBUG; - if (@ex_buf > 0) { - pop @ex_buf; - _set_prompt(':' . join '', @ex_buf); - # Backspacing over : exits ex-mode. - } else { - _update_mode(M_CMD); - } + print '%_vim_mode: ', $warning, '%_'; +} - # Return key - execute command - } elsif ($key == 10) { - print "Run ex-mode command" if DEBUG; - cmd_ex_command(); - _update_mode(M_CMD); +sub _debug { + return unless DEBUG; - } elsif ($key == 9) { # TAB - print "Tab pressed" if DEBUG; - print "Ex buf contains: " . join('', @ex_buf) if DEBUG; - @tab_candidates = _tab_complete(join('', @ex_buf), [keys %$commands_ex]); - _debug("Candidates: " . join(", ", @tab_candidates)); - if (@tab_candidates == 1) { - @ex_buf = ( split('', $tab_candidates[0]), ' '); - _set_prompt(':' . join '', @ex_buf); - } - # Ignore control characters for now. - } elsif ($key > 0 && $key < 32) { - # TODO: use them later, e.g. completion + my ($format, @args) = @_; + my $str = sprintf($format, @args); + print $str; +} - # Append entered key +sub _command_with_context { + my ($command) = @_; + my $context; + my $window = Irssi::active_win; + if (defined $window) { + my $witem = $window->{active}; + if (defined $witem and ref($witem) eq 'Irssi::Windowitem') { + $context = $witem; + } else { + $context = $window; + } } else { - if ($key != -1) { - # check we're not called from an ex_history_* function - push @ex_buf, chr $key; + my $server = Irssi::active_server; + if (defined $server) { + $context = $server; } - _set_prompt(':' . join '', @ex_buf); } - - Irssi::statusbar_items_redraw("vim_windows"); - - _stop(); + if (defined $context) { + print "Command $command Using context: " . ref($context) if DEBUG; + $context->command($command); + } else { + print "Command $command has no context" if DEBUG; + Irssi::command($command); + } } -sub _tab_complete { - my ($input, $source) = @_; - my @out; - foreach my $item (@$source) { - if ($item =~ m/^\Q$input\E/) { - push @out, $item; - } - } +sub ex_history_add { + my ($line) = @_; - return sort { $a cmp $b } @out; -} + # check it's not an exact dupe of the previous history line -sub vim_mode_init { - Irssi::signal_add_first 'gui key pressed' => \&got_key; - Irssi::statusbar_item_register ('vim_mode', 0, 'vim_mode_cb'); - Irssi::statusbar_item_register ('vim_windows', 0, 'b_windows_cb'); + my $last_hist = $ex_history[$ex_history_index]; + $last_hist = '' unless defined $last_hist; - Irssi::expando_create('vim_cmd_mode' => \&vim_exp_mode, {}); - Irssi::expando_create('vim_wins' => \&vim_exp_wins, {}); + return if $last_hist eq $line; + _debug("Adding $line to ex command history"); - # Register all available settings. - foreach my $name (keys %$settings) { - _setting_register($name); + # add it to the history + unshift @ex_history, $line; + + if ($settings->{ex_history_size}->{value} < @ex_history) { + pop @ex_history; # junk the last entry if we've hit the max. } +} - foreach my $char ('a' .. 'z') { - $registers->{$char} = ''; +sub ex_history_fwd { + + my $hist_max = $#ex_history; + $ex_history_index++; + if ($ex_history_index > $hist_max) { + $ex_history_index = 0; + _debug("ex history hit top, wrapping to 0"); } - setup_changed(); + my $line = $ex_history[$ex_history_index]; + $line = '' if not defined $line; - Irssi::signal_add 'setup changed' => \&setup_changed; + _debug("Ex history line: $line"); + + @ex_buf = split '', $line; + handle_command_ex(-1); +} + +sub ex_history_back { + my $hist_max = $#ex_history; + $ex_history_index--; + if ($ex_history_index == -1) { + $ex_history_index = $hist_max; + _debug("ex history hit bottom, wrapping to $hist_max"); - # Add all default mappings. - foreach my $char (keys %$commands) { - next if $char =~ /^_/; # skip private commands (text-objects for now) - add_map($char, $commands->{$char}); } - # Load the vim_moderc file if it exists. - ex_source('source'); + my $line = $ex_history[$ex_history_index]; + $line = '' if not defined $line; - setup_changed(); - _reset_undo_buffer(); + _debug("Ex history line: $line"); + @ex_buf = split '', $line; + handle_command_ex(-1); - if ($settings->{start_cmd}->{value}) { - _update_mode(M_CMD); - } else { - _update_mode(M_INS); +} + +sub ex_history_show { + my $win = Irssi::active_win(); + $win->print("Ex command history:"); + for my $i (0 .. $#ex_history) { + my $flag = $i == $ex_history_index + ? ' <' + : ''; + $win->print("$i " . $ex_history[$i] . $flag); } } -sub setup_changed { - my $value; - if ($settings->{cmd_seq}->{value} ne '') { - delete $imaps->{$settings->{cmd_seq}->{value}}; - } - $value = _setting_get('cmd_seq'); - if ($value eq '') { - $settings->{cmd_seq}->{value} = $value; - } else { - if (length $value == 1) { - $imaps->{$value} = { 'map' => $value, - 'func' => sub { _update_mode(M_CMD) } - }; - $settings->{cmd_seq}->{value} = $value; - } else { - _warn("Error: vim_mode_cmd_seq must be a single character"); - # Restore the value so $settings and irssi settings are - # consistent. - _setting_set('cmd_seq', $settings->{cmd_seq}->{value}); - } - } +################################################################ +# INPUT HANDLING # +################################################################ + + +sub sig_gui_keypress { + my ($key) = @_; + + return if ($should_ignore); + + # Esc key + if ($key == 27) { + print "Esc seen, starting buffer" if DEBUG; + $input_buf_enabled = 1; + + # NOTE: this timeout might be too low on laggy systems, but + # it comes at the cost of keystroke latency for things that + # contain escape sequences (arrow keys, etc) + my $esc_buf_timeout = $settings->{esc_buf_timeout}->{value}; + + $input_buf_timer + = Irssi::timeout_add_once($esc_buf_timeout, + \&handle_input_buffer, undef); - my $new_utf8 = _setting_get('utf8'); - if ($new_utf8 != $settings->{utf8}->{value}) { - # recompile the patterns when switching to/from utf-8 - $word = qr/[\w_]/o; - $non_word = qr/[^\w_\s]/o; + print "Buffer Timer tag: $input_buf_timer" if DEBUG; - $settings->{utf8}->{value} = $new_utf8; - } - 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."); - } + } elsif ($mode == M_INS) { - # Sync $settings with current irssi values. - foreach my $name (keys %$settings) { - # These were already handled above. - next if $name eq 'cmd_seq' or $name eq 'cmd_seq'; + if ($key == 3) { # Ctrl-C enters command mode + _update_mode(M_CMD); + _stop(); + return; - $settings->{$name}->{value} = _setting_get($name); - } -} + } elsif ($key == 10) { # enter. + _commit_line(); -sub UNLOAD { - Irssi::signal_remove('gui key pressed' => \&got_key); - Irssi::signal_remove('setup changed' => \&setup_changed); - Irssi::statusbar_item_unregister ('vim_mode'); - Irssi::statusbar_item_unregister ('vim_windows'); -} + } elsif ($input_buf_enabled and $imap) { + print "Imap $imap active" if DEBUG; + my $map = $imaps->{$imap}; + 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 { + push @input_buf, $key; + } + flush_input_buffer(); + _stop(); + $imap = undef; + return; -sub _add_undo_entry { - my ($line, $pos) = @_; + } elsif (exists $imaps->{chr($key)}) { + print "Imap " . chr($key) . " seen, starting buffer" if DEBUG; - # 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--; + # start imap pending mode + $imap = chr($key); + + $input_buf_enabled = 1; + push @input_buf, $key; + $input_buf_timer + = Irssi::timeout_add_once(1000, \&flush_input_buffer, undef); + + _stop(); + return; + + # Pressing delete resets insert mode repetition (8 = BS, 127 = DEL). + # TODO: maybe allow it + } elsif ($key == 8 || $key == 127) { + @insert_buf = (); + # All other entered characters need to be stored to allow repeat of + # insert mode. Ignore delete and control characters. + } elsif ($key > 31) { + push @insert_buf, chr($key); + } } - # 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; + if ($input_buf_enabled) { + push @input_buf, $key; + _stop(); + return; } - my $max = $settings->{max_undo_lines}->{value}; -} -sub _restore_undo_entry { - my $entry = $undo_buffer[$undo_index]; - _input($entry->[0]); - _input_pos($entry->[1]); -} + if ($mode == M_CMD) { + my $should_stop = handle_command_cmd($key); + _stop() if $should_stop; + Irssi::statusbar_items_redraw("vim_mode"); -sub _print_undo_buffer { + } elsif ($mode == M_EX) { - my $i = 0; - my @buf; - foreach my $entry (@undo_buffer) { - my $str = ''; - if ($i == $undo_index) { - $str .= '* '; - } else { - $str .= ' '; + if ($key == 3) { # C-c cancels Ex mdoe as well. + _update_mode(M_CMD); + _stop(); + return; } - my ($line, $pos) = @$entry; - substr($line, $pos, 0) = '*'; - # substr($line, $pos+3, 0) = '%_'; - $str .= sprintf('%02d %s [%d]', $i, $line, $pos); - push @buf, $str; - $i++; + handle_command_ex($key); } - print "------ undo buffer ------"; - print join("\n", @buf); - print "------------------ ------"; - } -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; -} +# TODO: merge this with 'flush_input_buffer' below. -sub add_map { - my ($keys, $command) = @_; +sub handle_input_buffer { - # 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} = { char => _parse_mapping_reverse($tmp), - cmd => undef, - maps => {} - }; - } - if (not exists $maps->{$tmp}->{maps}->{$tmp . $map}) { - $maps->{$tmp}->{maps}->{$tmp . $map} = undef; - } - } + #Irssi::timeout_remove($input_buf_timer); + $input_buf_timer = undef; + # see what we've collected. + print "Input buffer contains: ", join(", ", @input_buf) if DEBUG; - if (not exists $maps->{$keys}) { - $maps->{$keys} = { char => undef, - cmd => undef, - maps => {} - }; - } - $maps->{$keys}->{char} = _parse_mapping_reverse($keys); - $maps->{$keys}->{cmd} = $command; -} + if (@input_buf == 1 && $input_buf[0] == 27) { -sub delete_map { - my ($keys) = @_; + print "Enter Command Mode" if DEBUG; + _update_mode(M_CMD); - # Abort for non-existent mappings or placeholder mappings. - return if not exists $maps->{$keys} or not defined $maps->{$keys}->{cmd}; + } else { + # we have more than a single esc, implying an escape sequence + # (meta-* or esc-*) - my @add = (); + # currently, we only extract escape sequences if: + # a) we're in ex mode + # b) they're arrow keys (for history control) - # If no maps need the current key, then remove it and all other - # unnecessary keys in the "tree". - if (keys %{$maps->{$keys}->{maps}} == 0) { - my $tmp = $keys; - while (length $tmp > 1) { - my $map = substr $tmp, -1, 1, ''; - delete $maps->{$tmp}->{maps}->{$tmp . $map}; - if (not $maps->{$tmp}->{cmd} and keys %{$maps->{$tmp}->{maps}} == 0) { - push @add, $tmp; - delete $maps->{$tmp}; - } else { - last; + if ($mode == M_EX) { + # ex mode + my $key_str = join '', map { chr } @input_buf; + if ($key_str =~ m/^\e\[([ABCD])/) { + my $arrow = $1; + _debug( "Arrow key: $arrow"); + if ($arrow eq 'A') { # up + ex_history_back(); + } elsif ($arrow eq 'B') { # down + ex_history_fwd(); + } else { + $arrow =~ s/C/right/; + $arrow =~ s/D/left/; + _debug("Arrow key $arrow not supported"); + } } + } else { + # otherwise, we just forward them to irssi. + _emulate_keystrokes(@input_buf); } - } - if (keys %{$maps->{$keys}->{maps}} > 0) { - $maps->{$keys}->{cmd} = undef; - } else { - delete $maps->{$keys}; + # Clear insert buffer, pressing "special" keys (like arrow keys) + # resets it. + @insert_buf = (); } - push @add, $keys; - # Restore default keybindings in case we :unmapped a or a remapped - # key. - foreach my $key (@add) { - if (exists $commands->{$key}) { - add_map($key, $commands->{$key}); - } - } + @input_buf = (); + $input_buf_enabled = 0; } +sub flush_input_buffer { + Irssi::timeout_remove($input_buf_timer) if defined $input_buf_timer; + $input_buf_timer = undef; + # see what we've collected. + print "Input buffer flushed" if DEBUG; -sub _commit_line { - _update_mode(M_INS); + # Add the characters to @insert_buf so they can be repeated. + push @insert_buf, map chr, @input_buf; - # separate from call above as _update_mode() does additional internal work - # and we need to make sure it gets correctly called. - _update_mode(M_CMD) if $settings->{start_cmd}->{value}; + _emulate_keystrokes(@input_buf); - _reset_undo_buffer('', 0); + @input_buf = (); + $input_buf_enabled = 0; + + $imap = undef; } -sub _input { - my ($data) = @_; +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; - my $current_data = Irssi::parse_special('$L', 0, 0); + handle_command_cmd(undef); + Irssi::statusbar_items_redraw("vim_mode"); +} - if ($settings->{utf8}->{value}) { - $current_data = decode_utf8($current_data); - } +sub handle_numeric_prefix { + my ($char) = @_; + my $num = 0+$char; - if (defined $data) { - if ($settings->{utf8}->{value}) { - Irssi::gui_input_set(encode_utf8($data)); - } else { - Irssi::gui_input_set($data); - } + if (defined $numeric_prefix) { + $numeric_prefix *= 10; + $numeric_prefix += $num; } else { - $data = $current_data; + $numeric_prefix = $num; } - - return $data; } -sub _input_len { - return length _input(); -} +sub handle_command_cmd { + my ($key) = @_; -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; + my $pending_map_flushed = 0; - 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; + my $char; + if (defined $key) { + $char = chr($key); + # We were called from flush_pending_map(). } else { - $pos = $cur_pos; - #print "Input pos retrieved as $pos" if DEBUG; + $char = $pending_map; + $key = 0; + $pending_map_flushed = 1; } - return $pos; -} - -sub _emulate_keystrokes { - my @keys = @_; - $should_ignore = 1; - for my $key (@keys) { - Irssi::signal_emit('gui key pressed', $key); + # Counts + if (!$movement and !$pending_map 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() } - $should_ignore = 0; -} -sub _stop() { - Irssi::signal_stop_by_name('gui key pressed'); -} + if (defined $pending_map and not $pending_map_flushed) { + $pending_map = $pending_map . $char; + $char = $pending_map; + } -sub _update_mode { - my ($new_mode) = @_; + my $map; + if ($movement) { + $map = { char => $movement->{char}, + cmd => $movement, + maps => {}, + }; - my $pos; + } elsif (exists $maps->{$char}) { + $map = $maps->{$char}; - 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; + # 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; + } - # 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); + # 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() } - # 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) { - $numeric_prefix = undef; - $operator = undef; - $movement = undef; - $register = '"'; + } else { + print "No mapping found for $char" if DEBUG; $pending_map = undef; - - # Also clear ex-mode buffer. - @ex_buf = (); + $numeric_prefix = undef; + return 1; # call _stop() } - Irssi::statusbar_items_redraw("vim_mode"); - Irssi::statusbar_items_redraw ('uberprompt'); - -} + $pending_map = undef; -sub _set_prompt { - my $msg = shift; + my $cmd = $map->{cmd}; - # add a leading space unless we're trying to clear it entirely. - if (length($msg) and $settings->{prompt_leading_space}->{value}) { - $msg = ' ' . $msg; + # 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() } - # escape % symbols. This prevents any _set_prompt calls from using - # colouring sequences. - $msg =~ s/%/%%/g; + # Ex-mode commands can also be bound in command mode. + if ($cmd->{type} == C_EX) { + print "Processing ex-command: $map->{char} ($cmd->{char})" if DEBUG; - Irssi::signal_emit('change prompt', $msg, 'UP_INNER'); -} + $cmd->{func}->(substr($cmd->{char}, 1), $numeric_prefix); + $numeric_prefix = undef; -sub _setting_get { - my ($name) = @_; + return 1; # call _stop() + # As can irssi commands. + } elsif ($cmd->{type} == C_IRSSI) { + print "Processing irssi-command: $map->{char} ($cmd->{char})" if DEBUG; - my $type = $settings->{$name}->{type}; - $name = "vim_mode_$name"; + _command_with_context($cmd->{func}); - my $ret = undef; + $numeric_prefix = undef; + return 1; # call _stop(); + # does nothing. + } elsif ($cmd->{type} == C_NOP) { + print "Processing : $map->{char}" if DEBUG; - if ($type == S_BOOL) { - $ret = Irssi::settings_get_bool($name); - } elsif ($type == S_INT) { - $ret = Irssi::settings_get_int($name); - } elsif ($type == S_STR) { - $ret = Irssi::settings_get_str($name); - } else { - _warn("Unknown setting type '$type', please report."); + $numeric_prefix = undef; + return 1; # call _stop(); } - return $ret; -} - -sub _setting_set { - my ($name, $value) = @_; - - my $type = $settings->{$name}->{type}; - $name = "vim_mode_$name"; + # 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; + } - if ($type == S_BOOL) { - Irssi::settings_set_bool($name, $value); - } elsif ($type == S_INT) { - Irssi::settings_set_int($name, $value); - } elsif ($type == S_STR) { - Irssi::settings_set_str($name, $value); - } else { - _warn("Unknown setting type '$type', please report."); - } -} -sub _setting_register { - my ($name) = @_; + } 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 $value = $settings->{$name}->{value}; - my $type = $settings->{$name}->{type}; - $name = "vim_mode_$name"; + my $pos = _input_pos(); + $cmd->{func}->(0, _input_len(), undef, 0); + # Restore position for yy. + if ($cmd == $commands->{y}) { + _input_pos($pos); + # And save undo for other operators. + } else { + _add_undo_entry(_input(), _input_pos()); + } + if ($register ne '"') { + print 'Changing register to "' if DEBUG; + $register = '"'; + } + } + $numeric_prefix = undef; + $operator = undef; + $movement = undef; + # Set new operator. + } else { + $operator = $cmd; + } - if ($type == S_BOOL) { - Irssi::settings_add_bool('vim_mode', $name, $value); - } elsif ($type == S_INT) { - Irssi::settings_add_int('vim_mode', $name, $value); - } elsif ($type == S_STR) { - Irssi::settings_add_str('vim_mode', $name, $value); - } else { - _warn("Unknown setting type '$type', please report."); - } -} + # Start Ex mode. + } elsif ($cmd == $commands->{':'}) { -sub _warn { - my ($warning) = @_; + if (not script_is_loaded('uberprompt')) { + _warn("Warning: Ex mode requires the 'uberprompt' script. " . + "Please load it and try again."); + } else { + _update_mode(M_EX); + _set_prompt(':'); + } - print '%_vim_mode: ', $warning, '%_'; -} + # Enter key sends the current input line in command mode as well. + } elsif ($key == 10) { + _commit_line(); + return 0; # don't call _stop() -sub _debug { - return unless DEBUG; + } else { + print "Processing command: $map->{char} ($cmd->{char})" if DEBUG; - my ($format, @args) = @_; - my $str = sprintf($format, @args); - print $str; -} + my $skip = 0; + my $repeat = 0; -sub _command_with_context { - my ($command) = @_; - my $context; - my $window = Irssi::active_win; - if (defined $window) { - my $witem = $window->{active}; - if (defined $witem and ref($witem) eq 'Irssi::Windowitem') { - $context = $witem; - } else { - $context = $window; + 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; + } } - } else { - my $server = Irssi::active_server; - if (defined $server) { - $context = $server; + + # Ignore invalid operator/command combinations. + if ($operator and $cmd->{no_operator}) { + print "Invalid operator/command: $operator->{char} $cmd->{char}" + if DEBUG; + $skip = 1; } - } - if (defined $context) { - print "Command $command Using context: " . ref($context) if DEBUG; - $context->command($command); - } else { - print "Command $command has no context" if DEBUG; - Irssi::command($command); - } -} -sub ex_history_add { - my ($line) = @_; + 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; + } - # check it's not an exact dupe of the previous history line + my $cur_pos = _input_pos(); - my $last_hist = $ex_history[$ex_history_index]; - $last_hist = '' unless defined $last_hist; + # 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 { + ($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(); + } - return if $last_hist eq $line; + # 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; + } - _debug("Adding $line to ex command history"); + # 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; + $operator->{func}->($cur_pos, $new_pos, $cmd, $repeat); + } - # add it to the history - unshift @ex_history, $line; + # 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 history entries still show up in undo + # buffer? Is avoiding the commands here insufficient? - if ($settings->{ex_history_size}->{value} < @ex_history) { - pop @ex_history; # junk the last entry if we've hit the max. - } -} + _add_undo_entry(_input(), _input_pos()); + } -sub ex_history_fwd { + # 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; + } + } - my $hist_max = $#ex_history; - $ex_history_index++; - if ($ex_history_index > $hist_max) { - $ex_history_index = 0; - _debug("ex history hit top, wrapping to 0"); - } + # 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; - my $line = $ex_history[$ex_history_index]; - $line = '' if not defined $line; + if ($cmd != $commands->{'"'} and $register ne '"') { + print 'Changing register to "' if DEBUG; + $register = '"'; + } - _debug("Ex history line: $line"); + } - @ex_buf = split '', $line; - handle_command_ex(-1); + return 1; # call _stop() } -sub ex_history_back { - my $hist_max = $#ex_history; - $ex_history_index--; - if ($ex_history_index == -1) { - $ex_history_index = $hist_max; - _debug("ex history hit bottom, wrapping to $hist_max"); - - } +sub handle_command_ex { + my ($key) = @_; - my $line = $ex_history[$ex_history_index]; - $line = '' if not defined $line; + # BS key (8) or DEL key (127) - remove last character. + if ($key == 8 || $key == 127) { + print "Delete" if DEBUG; + if (@ex_buf > 0) { + pop @ex_buf; + _set_prompt(':' . join '', @ex_buf); + # Backspacing over : exits ex-mode. + } else { + _update_mode(M_CMD); + } - _debug("Ex history line: $line"); - @ex_buf = split '', $line; - handle_command_ex(-1); + # Return key - execute command + } elsif ($key == 10) { + print "Run ex-mode command" if DEBUG; + cmd_ex_command(); + _update_mode(M_CMD); -} + } elsif ($key == 9) { # TAB + print "Tab pressed" if DEBUG; + print "Ex buf contains: " . join('', @ex_buf) if DEBUG; + @tab_candidates = _tab_complete(join('', @ex_buf), [keys %$commands_ex]); + _debug("Candidates: " . join(", ", @tab_candidates)); + if (@tab_candidates == 1) { + @ex_buf = ( split('', $tab_candidates[0]), ' '); + _set_prompt(':' . join '', @ex_buf); + } + # Ignore control characters for now. + } elsif ($key > 0 && $key < 32) { + # TODO: use them later, e.g. completion -sub ex_history_show { - my $win = Irssi::active_win(); - $win->print("Ex command history:"); - for my $i (0 .. $#ex_history) { - my $flag = $i == $ex_history_index - ? ' <' - : ''; - $win->print("$i " . $ex_history[$i] . $flag); + # Append entered key + } else { + if ($key != -1) { + # check we're not called from an ex_history_* function + push @ex_buf, chr $key; + } + _set_prompt(':' . join '', @ex_buf); } + + Irssi::statusbar_items_redraw("vim_windows"); + + _stop(); } + + + vim_mode_init(); -- cgit v1.2.3