File Coverage

blib/lib/Tk/TextVi.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Tk::TextVi;
2            
3 1     1   17979 use strict;
  1         2  
  1         34  
4 1     1   4 use warnings;
  1         2  
  1         34  
5            
6             our $VERSION = '0.015';
7            
8             #use Data::Dump qw|dump|;
9            
10 1     1   1858 use Tk;
  0            
  0            
11             use Tk::TextUndo ();
12             use base qw'Tk::Derived Tk::TextUndo';
13            
14             use Carp qw'carp croak';
15            
16             Construct Tk::Widget 'TextVi';
17            
18             # Constants for keys that Tk treats special
19             sub BKSP () { "\cH" }
20             sub TAB () { "\cI" }
21             sub ESC () { "\c[" }
22            
23             # Constants used for exceptions
24             sub X_NO_KEYS () { "VI_NO_KEYS\n" }
25             sub X_BAD_STATE () { "VI_BAD_STATE\n" }
26             sub X_NO_MOTION () { "VI_NO_MOTION\n" }
27            
28             # Constants used for flags
29             sub F_STAT () { 1 }
30             sub F_MSG () { 2 }
31             sub F_ERR () { 4 }
32            
33             # Indentifier-legal names for special characters
34             my %names = (
35             '<' => '_lt',
36             '>' => '_gt',
37             );
38            
39             my %settings = (
40             # name => [ value, default, type ]
41             # name => \'longname'
42            
43             'softtabstop' => [ 4, 4, 'int' ],
44             'sts' => \'softtabstop',
45             );
46            
47             # Default command mappings and what file holds their test data
48             my %map = (
49             n => {
50             a => \&vi_n_a, # 30-insert
51             b => \'B',
52             d => \&vi_n_d, # 20-delete
53             e => \'E',
54             f => \&vi_n_f, # 13-findchar
55             g => {
56             a => \&vi_n_ga, # 60-info
57             g => \&vi_n_gg, # 10-move
58             },
59             h => \&vi_n_h, # 10-move
60             i => \&vi_n_i, # 30-insert
61             j => \&vi_n_j, # 10-move
62             k => \&vi_n_k, # 10-move
63             l => \&vi_n_l, # 10-move
64             m => \&vi_n_m, # 11-mark
65             n => \&vi_n_n, # 15-search
66             o => \&vi_n_o, # 30-insert
67             p => \&vi_n_p, # 40-register
68             q => \&vi_n_q, # 41-macro
69             r => \&vi_n_r, # 21-replace
70             t => \&vi_n_t, # 13-findchar
71             u => \&vi_n_u,
72             v => \&vi_n_v, # 32-vchar
73             w => \'W',
74             x => \'dl', # 20-delete
75             y => \&vi_n_y, # 40-register
76            
77             A => \'$a',
78             B => \&vi_n_B,
79             D => \'d$', # 20-delete
80             E => \&vi_n_E, # 12-word
81             G => \&vi_n_G, # 10-move
82             I => \&vi_n_I,
83             O => \&vi_n_O, # 30-insert
84             R => \&vi_n_R,
85             V => \&vi_n_V, # 33-vline
86             W => \&vi_n_W, # 12-word
87            
88             0 => [ 'insert linestart', 'char', 'inc' ], # 10-move
89            
90             '~' => \&vi_n_tilde,
91             '`' => \&vi_n_backtick, # 11-mark
92             '@' => \&vi_n_at, # 41-macro
93             '$' => \&vi_n_dollar, # 10-move
94             '%' => \&vi_n_percent, # 14-findline
95             ':' => \&vi_n_colon,
96             '/' => \&vi_n_fslash, # 15-search
97             },
98             c => {
99             '' => \&vi_c_none,
100             map => \&vi_c_map,
101             noh => \&vi_c_nohlsearch,
102             nohl => \&vi_c_nohlsearch,
103             nohlsearch => \&vi_c_nohlsearch,
104             set => \&vi_c_set,
105             split => \&vi_c_split,
106             },
107             v => {
108             d => \&vi_n_d,
109             f => \&vi_n_f,
110             e => \'E',
111             g => {
112             g => [ '1.0', 'line', 'inc' ],
113             },
114             h => \&vi_n_h,
115             j => \&vi_n_j,
116             k => \&vi_n_k,
117             l => \&vi_n_l,
118             t => \&vi_n_t,
119             r => \&vi_n_r,
120             w => \'W',
121             y => \&vi_n_y,
122            
123             E => \&vi_n_E,
124             G => \&vi_n_G,
125             W => \&vi_n_W,
126            
127             0 => [ 'insert linestart', 'char', 'inc' ],
128            
129             '~' => \&vi_n_tilde,
130             '`' => \&vi_n_backtick,
131             '$' => \&vi_n_dollar,
132             '%' => \&vi_n_percent,
133             '"' => \&vi_n_quote,
134             ':' => \&vi_n_colon,
135             }
136             );
137            
138             # Tk derived class initializer
139             sub ClassInit {
140             my( $self, $mw ) = @_;
141            
142             $self->SUPER::ClassInit( $mw );
143            
144             # TODO: Kill default Tk::Text Bindings
145            
146             # Convert keys that Tk handles specially into normal keys
147             # TODO: Add missing keys
148             $mw->bind( $self, '', [ 'InsertKeypress', BKSP ] );
149             $mw->bind( $self, '', [ 'InsertKeypress', TAB ] );
150             $mw->bind( $self, '', [ 'InsertKeypress', ESC ] );
151             $mw->bind( $self, '', [ 'InsertKeypress', "\n" ] );
152            
153             # Rebind the control keys to give characters
154             # TODO: Add remaining
155             $mw->bind( $self, '', [ 'InsertKeypress', "\cO" ] );
156            
157             return $self;
158             }
159            
160             # Constructor
161             sub Populate {
162             my ($w,$args) = @_;
163            
164             $w->SUPER::Populate( $args );
165            
166             $w->{VI_PENDING} = ''; # Pending command
167             $w->{VI_MODE} = 'n'; # Start in normal mode
168             $w->{VI_SUBMODE} = ''; # No submode
169             $w->{VI_REGISTER} = { }; # Empty registers
170             $w->{VI_SETTING} = { }; # No settings
171             $w->{VI_ERROR} = [ ]; # Pending errors
172             $w->{VI_MESSAGE} = [ ]; # Pending messages
173             $w->{VI_FLAGS} = 0; # Pending flags
174             $w->{VI_COMMANDS} = { }; # External commands
175            
176             # XXX: There might be a legit reason in the future to have two
177             # Tk::TextVi widgets with different mappings.
178             $w->{VI_MAPS} = \%map; # Command mapping
179            
180             $w->tagConfigure( 'VI_SEARCH', -background => '#FFFF00' );
181            
182             $w->ConfigSpecs(
183             -statuscommand =>['CALLBACK','statusCommand', 'StatusCommand', 'NoOp'],
184             -messagecommand =>['CALLBACK','messageCommand','MessageCommand','NoOp'],
185             -errorcommand => ['CALLBACK','errorCommand', 'ErrorCommand', 'NoOp'],
186             -commands => ['METHOD', 'commands', 'Commands', {} ],
187             );
188             }
189            
190             # Config commands
191             sub commands {
192             my $w = shift;
193            
194             if( @_ >= 2 ) {
195             if( @_ % 2 == 1 ) {
196             croak "Tk::TextVi commands() received odd number of arguments."
197             }
198            
199             my %commands = @_;
200            
201             for my $cmd ( keys %commands ) {
202             my $sub = $commands{$cmd};
203            
204             if( 'CODE' eq ref $sub ) {
205             $w->{VI_COMMANDS}{$cmd} = $sub;
206             }
207             elsif( not defined $sub ) {
208             delete $w->{VI_COMMANDS}{$cmd};
209             }
210             else {
211             croak "Tk::TextVi commands() expected coderef or undef, got '$sub'";
212             }
213             }
214             }
215             elsif( @_ == 1 ) {
216             if( 'HASH' ne ref $_[0] ) {
217             croak "Tk::TextVi commands() expected hashref or pairs, got '$_[0]'";
218             }
219            
220             for my $sub ( values %{ $_[0] } ) {
221             croak "Tk::TextVi commands() expected coderef, got '$sub'";
222             }
223            
224             %{ $w->{VI_COMMANDS} } = %{ $_[0] };
225             }
226            
227             return %{ $w->{VI_COMMANDS} };
228             }
229            
230             # We don't want to lose the selection.
231             # Movement commands extend visual selection
232             sub SetCursor {
233             my($w,$pos) = @_;
234             $pos = 'end -1c' if $w->compare($pos,'==','end');
235             $w->markSet('insert',$pos);
236             $w->see('insert');
237            
238             if( $w->{VI_MODE} eq 'v' ) {
239             $w->tagRemove( 'sel', '1.0', 'end' );
240            
241             my ($s,$e) = ($w->{VI_VISUAL_START}, 'insert');
242            
243             if( $w->compare( $e, '<', $s ) ) {
244             ($s,$e) = ($e,$s);
245             }
246            
247             $w->tagAdd( 'sel', $s, $e );
248            
249             $w->markSet( 'VI_MARK__lt', $s );
250             $w->markSet( 'VI_MARK__gt', $e );
251             }
252             elsif( $w->{VI_MODE} eq 'V' ) {
253             $w->tagRemove( 'sel', '1.0', 'end' );
254            
255             my ($s,$e) = ($w->{VI_VISUAL_START}, 'insert');
256             if( $w->compare( $e, '<', $s ) ) {
257             ($s,$e) = ($e,$s);
258             }
259            
260             $w->tagAdd( 'sel', "$s linestart", "$e lineend" );
261            
262             $w->markSet( 'VI_MARK__lt', $s );
263             $w->markSet( 'VI_MARK__gt', $e );
264             }
265             }
266            
267             # Deep Experimental Magic
268             #
269             # Only invoke the special split-variant functions when we're using
270             # :split
271             my @split_func = qw| delete insert tagAdd tagConfigure tagRemove |;
272            
273             {
274             no strict;
275             for my $func ( @split_func ) {
276             *{ "split_$func" } = sub {
277             my ($w,@args) = @_;
278            
279             if( defined $w->{VI_SPLIT_SHARE} ) {
280             for my $win ( @{ $w->{VI_SPLIT_SHARE} } ) {
281             "Tk::TextUndo::$func"->( $win, @args );
282             }
283             }
284             else {
285             "Tk::TextUndo::$func"->( $w, @args );
286             }
287             }
288             }
289             }
290            
291             sub vi_split {
292             my ($w,$newwin) = @_;
293            
294             # First time replace all the functions with the magical split versions
295             if( not defined $w->{VI_SPLIT_SHARE} ) {
296             $w->{VI_SPLIT_SHARE} = [ $w ];
297            
298             no strict;
299             for my $func (@split_func) {
300             *{"Tk::TextVi::$func"} = \&{"split_$func"};
301             }
302             }
303            
304             $newwin->Contents( $w->Contents );
305             $newwin->SetCursor( $w->index('insert') );
306             $newwin->yviewMoveto( ($w->yview)[0] );
307            
308             push @{$w->{VI_SPLIT_SHARE}}, $newwin;
309             $newwin->{VI_SPLIT_SHARE} = $w->{VI_SPLIT_SHARE}
310             }
311            
312             # Public Methods #####################################################
313            
314             sub viMode {
315             my ($w, $mode) = @_;
316             my $rv = $w->{VI_SUBMODE} . $w->{VI_MODE};
317             $rv .= 'q' if defined $w->{VI_RECORD_REGISTER};
318            
319             if( defined $mode ) {
320             croak "Tk::TextVi received invalid mode '$mode'"
321             if $mode !~ m[ ^ [nicvVR/] $ ]x;
322             $w->{VI_MODE} = $mode;
323             $w->{VI_SUBMODE} = '';
324             $w->{VI_PENDING} = '';
325             $w->{VI_REPLACE_CHARS} = '';
326             $w->tagRemove( 'sel', '1.0', 'end' );
327            
328             # XXX: Hack
329             if( (caller)[0] eq 'Tk::TextVi' ) {
330             $w->{VI_FLAGS} |= F_STAT;
331             }
332             else {
333             # TODO: this is broken
334             $w->Callback( '-statuscommand', $w->{VI_MODE}, $w->{VI_PENDING} );
335             }
336             }
337            
338             return $rv;
339             }
340            
341             sub viPending {
342             my ($w) = @_;
343             return $w->{VI_PENDING};
344             }
345            
346             sub viError {
347             my ($w) = @_;
348             return shift @{ $w->{VI_ERROR} };
349             }
350            
351             sub viMessage {
352             my ($w) = @_;
353             return shift @{ $w->{VI_MESSAGE} };
354             }
355            
356             sub viMap {
357             my ( $w, $mode, $sequence, $ref, $force ) = @_;
358            
359             # TODO: nmap,imap,vmap etc. support
360             my @mapmodes = map { $w->{MAPS}{$_} } split //, $mode;
361            
362             while( length( $sequence ) > 1 ) {
363             # Get the next character in the sequence
364             my $c = substr $sequence, 0, 1, '';
365            
366             # Advance the mapping locations
367             for my $map ( @mapmodes ) {
368            
369             # Nothing at this location yet, add a hash
370             if( not defined $map->{$c} ) {
371             $map->{$c} = { };
372             }
373             # Something is already mapped here
374             elsif( 'HASH' ne ref $map->{$c} ) {
375             return unless $force;
376             # If $force was defined, nuke the previous entry
377             $map->{$c} = { };
378             }
379            
380             $map = $map->{$c};
381             }
382             }
383            
384             # Check that a mapping can be placed here
385             for my $map ( @mapmodes ) {
386             if( defined $map->{$sequence} and # Something is here
387             'HASH' eq ref $map->{$sequence} and # it's a longer mapping
388             scalar keys %{ $map->{$sequence}} ) # and its in use
389             {
390             return unless $force;
391             delete $map->{$sequence}; # wipe out existing maps
392             }
393             }
394            
395             for my $map ( @mapmodes ) {
396             $map->{$sequence} = $ref;
397             }
398            
399             # TODO: return the mappings that were replaced in a format that
400             # would permit them to be restored
401             return 1;
402             }
403            
404             # 'Private' Methods ##################################################
405            
406             # Store text in a register
407             #
408             # Caller is responsible for determining when text should also be
409             # written to the unnamed register or the small delete register at the
410             # moment (XXX: This should be handled here in the future)
411             sub registerStore {
412             my ( $w, $register, $text ) = @_;
413            
414             # Registers are all single characters or unnamed
415             die X_BAD_STATE if length($register) > 1;
416            
417             # Read-only registers and blackhole are never written to
418             return if $register =~ /[_:.%#0-9]/;
419            
420             # Always store in the unnamed register
421             $w->{VI_REGISTER}{''} = $text;
422            
423             # * is the clipboard
424             if( $register eq '*' ) {
425             $w->clipboardClear;
426             $w->clipboardAppend( '--', $text );
427             }
428             else {
429             if( $register =~ tr/A-Z/a-z/ ) {
430             $w->{VI_REGISTER}{$register} .= $text;
431             }
432             else {
433             $w->{VI_REGISTER}{$register} = $text;
434             }
435             }
436             }
437            
438             # Fetch the contents of a register
439             sub registerGet {
440             my ( $w, $register ) = @_;
441            
442             # Registers are single characters or unnamed
443             die X_BAD_STATE if length($register) > 1;
444            
445             # Nothing comes out of a black hole
446             return '' if $register eq '_';
447            
448             # TODO: other special registers
449            
450             # Register contains nothing
451             return '' unless defined $w->{VI_REGISTER}{$register};
452            
453             return $w->{VI_REGISTER}{$register};
454             }
455            
456             sub setMessage {
457             my ($w,$msg) = @_;
458            
459             push @{ $w->{VI_MESSAGE} }, $msg;
460             $w->{VI_FLAGS} |= F_MSG;
461             }
462            
463             sub setError {
464             my ($w,$msg) = @_;
465            
466             push @{ $w->{VI_ERROR} }, $msg;
467             $w->{VI_FLAGS} |= F_ERR;
468             }
469            
470             sub settingGet {
471             my ($w,$key) = @_;
472            
473             # TODO: decide what to do about widget-specific vs class settings
474             # possibly something like Vim's b: vs g:
475            
476             $key = ${ $settings{$key} } if 'SCALAR' eq ref $settings{$key};
477             return $settings{$key}[0];
478             }
479            
480             # Handle keyboard input
481             #
482             # Replaces method in Tk::Text
483             sub InsertKeypress {
484             my ($w,$key) = @_;
485             my $res;
486            
487             return if $key eq ''; # Ignore shift, control, etc.
488            
489             $w->{VI_RECORD_KEYS} .= $key if defined $w->{VI_RECORD_REGISTER};
490            
491             # Normal mode
492             if( $w->{VI_MODE} eq 'n' ) {
493             # Escape cancels any command in progress
494             if( $key eq ESC ) {
495             $w->viMode( $w->{VI_SUBMODE} || 'n' );
496             }
497             else {
498             $res = $w->InsertKeypressNormal( $key );
499            
500             # Array ref is returned by motion commands
501             if( 'ARRAY' eq ref $res ) {
502             $w->SetCursor( $res->[0] );
503             }
504             }
505             }
506             # Visual character mode
507             elsif( $w->{VI_MODE} eq 'v' ) {
508             if( $key eq ESC ) {
509             $w->viMode('n');
510             }
511             else {
512             $res = $w->InsertKeypressNormal( $key );
513            
514             if( 'ARRAY' eq ref $res ) {
515             $w->SetCursor( $res->[0] );
516             }
517             }
518             }
519             # Visual line mode
520             elsif( $w->{VI_MODE} eq 'V' ) {
521             if( $key eq ESC ) {
522             $w->viMode('n');
523             }
524             else {
525             $res = $w->InsertKeypressNormal( $key );
526            
527             if( 'ARRAY' eq ref $res ) {
528             $w->SetCursor( $res->[0] );
529             }
530             }
531             }
532             # Command mode
533             elsif( $w->{VI_MODE} eq 'c' ) {
534             if( $key eq BKSP ) {
535             if( $w->{VI_PENDING} eq '' ) {
536             $w->viMode('n');
537             }
538             else {
539             chop $w->{VI_PENDING};
540             }
541             }
542             elsif( $key eq "\n" ) {
543             $w->EvalCommand();
544             $w->viMode('n');
545             }
546             elsif( $key eq ESC ) {
547             $w->viMode('n');
548             }
549             else {
550             $w->{VI_PENDING} .= $key;
551             }
552             $w->{VI_FLAGS} |= F_STAT;
553             }
554             # Incremental search mode
555             elsif( $w->{VI_MODE} eq '/' ) {
556             if( $key eq BKSP ) {
557             if( $w->{VI_PENDING} eq '' ) {
558             $w->viMode('n');
559             }
560             else {
561             chop $w->{VI_PENDING};
562             }
563             $w->SetCursor( $w->vi_fslash() );
564             }
565             elsif( $key eq "\n" ) {
566             $w->vi_fslash_end;
567             $w->viMode('n');
568             }
569             elsif( $key eq ESC ) {
570             $w->viMode('n');
571             $w->SetCursor( $w->vi_fslash() );
572             }
573             else {
574             $w->{VI_PENDING} .= $key;
575             $w->SetCursor( $w->vi_fslash() );
576             }
577             $w->{VI_FLAGS} |= F_STAT;
578             }
579             # Insert mode
580             elsif( $w->{VI_MODE} eq 'i' ) {
581             if( $key eq ESC ) {
582             $w->addGlobEnd;
583             $w->viMode('n');
584             $w->SetCursor( 'insert -1c' )
585             if( $w->compare( 'insert', '!=', 'insert linestart' ) );
586             }
587             elsif( $key eq BKSP ) {
588             my $sts = $w->settingGet( 'softtabstop' );
589             if( $sts > 1 && ' ' eq $w->get( 'insert -1c' ) ) {
590             my $col = $w->index('insert');
591             (undef,$col) = split /\./, $col;
592            
593             if( $col > 0 ) {
594             $col = $col % $sts || $sts;
595             my $txt = $w->get( "insert - $col c", "insert" );
596             $txt =~ /(\s*)$/;
597             $col = length($1) || 1;
598             }
599             else {
600             $col = 1;
601             }
602             $w->delete( "insert - $col c", 'insert' );
603             }
604             else {
605             $w->delete( "insert -1c" );
606             }
607             }
608             elsif( $key eq TAB ) {
609             my $sts = $w->settingGet( 'softtabstop' );
610            
611             if( $sts > 0 ) {
612             my $col = $w->index('insert');
613             (undef,$col) = split /\./, $col;
614             # Perl's modulus is well behaved so this works fine
615             $col = (-$col % $sts) || $sts;
616             $w->insert( 'insert', ' ' x $col );
617             }
618             else {
619             $w->insert( 'insert', "\t" );
620             }
621             }
622             elsif( $key eq "\cO" ) {
623             $w->viMode('n');
624             $w->{VI_SUBMODE} = 'i';
625             }
626             else {
627             $w->insert( 'insert', $key );
628             $w->see( 'insert' );
629             }
630             }
631             elsif( $w->{VI_MODE} eq 'R') {
632             if( $key eq ESC ) {
633             $w->addGlobEnd;
634             $w->viMode('n');
635             $w->SetCursor( 'insert -1c' )
636             if( $w->compare( 'insert', '!=', 'insert linestart' ) );
637             }
638             elsif( $key eq BKSP ) {
639             my $r = chop $w->{VI_REPLACE_CHARS};
640             if( $r ne '' ) {
641             $w->delete( "insert -1c" );
642             if( $r ne "\0" ) {
643             $w->insert( 'insert', $r );
644             $w->SetCursor( 'insert -1c' );
645             }
646             }
647             else {
648             $w->SetCursor( 'insert -1c' );
649             }
650             }
651             elsif( $w->get( 'insert' ) ne "\n" ) {
652             $w->{VI_REPLACE_CHARS} .= $w->get( 'insert' );
653             $w->delete( 'insert' );
654             $w->insert( 'insert', $key );
655             }
656             else {
657             $w->{VI_REPLACE_CHARS} .= "\0";
658             $w->insert( 'insert', $key );
659             }
660             }
661             else {
662             die "Tk::TextVi internal state corrupted";
663             }
664            
665             # Does the UI need to update?
666             # XXX: HACK
667             if( (caller)[0] ne 'Tk::TextVi' ) {
668             $w->Callback( '-statuscommand',
669             $w->viMode,
670             $w->{VI_PENDING} ) if( $w->{VI_FLAGS} & F_STAT );
671             $w->Callback( '-messagecommand' ) if $w->{VI_FLAGS} & F_MSG ;
672             $w->Callback( '-errorcommand' ) if $w->{VI_FLAGS} & F_ERR ;
673            
674             $w->{VI_FLAGS} = 0;
675             }
676            
677             # Command may have moved insert cursor out of window
678             $w->see('insert');
679             }
680            
681             # Handles the command processing shared between Normal
682             # and visual mode commands
683             sub InsertKeypressNormal {
684             my ($w,$key) = @_;
685             my $res;
686            
687             my $keys = $w->{VI_PENDING} . $key;
688             $w->{VI_PENDING} = ''; # Assume command will work
689            
690             eval { $res = $w->EvalKeys($keys); };# try to process as a command
691            
692             if( $@ ) {
693             die $@ if $@ !~ /^VI_/; # wasn't our exception
694            
695             if( $@ eq X_NO_KEYS ) { # Restore pending keys
696             $w->{VI_PENDING} = $keys;
697             }
698             }
699             elsif ( lc $w->{VI_MODE} eq 'v' ) {
700             # hack, clear visual mode after command
701             ref $res or $w->viMode('n');
702             }
703             else {
704             # Restore mode
705             $w->viMode( $w->{VI_SUBMODE} ) if $w->{VI_SUBMODE};
706             }
707            
708             $w->{VI_FLAGS} |= F_STAT;
709             return $res;
710             }
711            
712             # Takes a string of keypresses and dispatches it to the right command
713             sub EvalKeys {
714             my ($w, $keys, $count, $register, $motion) = @_;
715             my $res;
716             my $mode = lc substr $w->{VI_MODE}, 0, 1; # V and v use the same maps
717            
718             $count = 0 unless defined $count;
719            
720             # Use the currently pending keys by default
721             $keys = $w->{VI_PENDING} unless defined $keys;
722            
723             # Extract the count
724             if( $keys =~ s/^([1-9]\d*)// ) {
725             $count ||= 1;
726             $count *= $1;
727             }
728            
729             # Extract the register
730             if( $keys =~ s/^"(.?)// ) {
731             $register = $1;
732             }
733            
734             die X_NO_KEYS if $keys eq ''; # No command here
735            
736             # What does this map too
737             $res = $w->{VI_MAPS}{$mode}{substr $keys, 0, 1, ''};
738            
739             # a hash ref is a multichar mapping, go deeper
740             while( 'HASH' eq ref $res ) {
741             die X_NO_KEYS if $keys eq '';
742             $res = $res->{substr $keys, 0, 1, ''};
743             }
744            
745             # If left with a function, call it
746             $res = $res->( $w, $keys, $count, $register, $motion )
747             if 'CODE' eq ref $res;
748            
749             # A stringy return means to use these keypresses instead
750             if( defined $res and 'SCALAR' eq ref $res ) {
751             $w->{VI_PENDING} = '';
752            
753             for my $key ( split //, $$res . $keys ) {
754             $w->InsertKeypress( $key );
755             }
756            
757             # The above call took care of everything
758             return;
759             }
760            
761             die X_BAD_STATE if $motion and 'ARRAY' ne ref $res;
762            
763             return $res;
764             }
765            
766             sub EvalCommand {
767             my ($w) = @_;
768            
769             my ($cmd,$force,$arg);
770            
771             local $_ = $w->{VI_PENDING};
772             my @range;
773            
774             # First attempt to extract a range
775             while (1) {
776             if( s/^\.// ) { push @range, 'insert' }
777             elsif( s/^\$// ) { push @range, 'end' }
778             elsif( s/^\%// ) { push @range, '1.0'; push @range, 'end' }
779             elsif( s/^(\d+)// ) { push @range, "$1.0"; }
780             elsif( s/^\'(.)// ) { push @range, "VI_MARK_" . ($names{$1}||$1) }
781             else { last }
782            
783             while( s/^([+-]\d+)// ) { $range[$#range] .= " $1 lines" }
784            
785             if( s/^[,;]// ) { redo }
786             };
787            
788             if( not m/
789             ^ # colon is not in the buffer
790             (\w*) # followed by the name of the command
791             (!?) # optional ! to force the command
792             (?:
793             \s* # space between command and argument
794             (.*) # everything else is the argument
795             )? # argument is optional
796             $
797             /x )
798             {
799             return; # Something's really screwed up
800             }
801            
802             $cmd = $1;
803             $force = 1 if $2;
804             $arg = $3;
805            
806             # Built-in command
807             if( exists $w->{VI_MAPS}{c}{$cmd} ) {
808             $w->{VI_MAPS}{c}{$cmd}( $w, $force, $arg, \@range );
809             }
810             # External command
811             else {
812             $w->vi_c( $cmd, $force, $arg, \@range );
813             }
814             }
815            
816             sub vi_c {
817             my ($w,$cmd,@args) = @_;
818            
819             if( exists $w->{VI_COMMANDS}{$cmd} ) {
820             return $w->{VI_COMMANDS}{$cmd}( $w, @args );
821             }
822             elsif( exists $w->{VI_COMMANDS}{NOT_SUPPORTED} ) {
823             return $w->{VI_COMMANDS}{NOT_SUPPORTED}( $w, $cmd, @args );
824             }
825             return;
826             }
827            
828             # All the normal-mode commands ######################################
829            
830             =begin comment
831            
832             sub vi_n_d {
833             my ($w,$k,$n,$r,$m) = @_;
834             die X_BAD_STATE if $m;
835             }
836            
837             =end comment
838            
839             =cut
840            
841             sub vi_n_a {
842             my ($w,$k,$n,$r,$m) = @_;
843             die X_BAD_STATE if $m;
844            
845             $w->SetCursor( 'insert +1c' )
846             unless $w->compare( 'insert', '==', 'insert lineend' );
847             $w->viMode('i');
848             }
849            
850             sub vi_n_d {
851             my ($w,$k,$n,$r,$m) = @_;
852             my ($start,$end,$wise,$type);
853             die X_BAD_STATE if $m;
854            
855             # In a visual mode we just need the selection
856             if( $w->{VI_MODE} eq 'v' ) {
857             $start = 'sel.first';
858             $end = 'sel.last';
859             $wise = 'char';
860             $type = 'exc';
861             }
862             elsif( $w->{VI_MODE} eq 'V' ) {
863             $start = 'sel.first';
864             $end = 'sel.last';
865             $wise = 'line';
866             }
867             # In normal mode there's more work
868             else {
869             # Special case, dd = delete line
870             if( $k eq 'd' ) {
871             # If not enough lines, don't delete anything
872             return if $n > int $w->index('end') - int $w->index('insert');
873            
874             $start = 'insert';
875             $end = 'insert';
876             $end .= '+' . ($n-1) . 'l' if $n > 1;
877             $wise = 'line';
878             }
879             else {
880             my $res = EvalKeys( @_[0 .. 3], 1 );
881            
882             $start = 'insert';
883             ($end,$wise,$type) = @$res;
884             }
885             }
886            
887             # Swap start and end if the motion was backwards
888             if( $w->compare( $start, '>', $end ) ) {
889             ($start,$end) = ($end,$start);
890             $type = 'exc'; # XXX: hack
891             }
892            
893             if( $wise eq 'line' ) {
894            
895             $start .= ' linestart'; # From start of line
896             $end .= ' lineend +1c'; # Including the \n of the final line
897             }
898             else {
899             $end .= ' +1c' if $type eq 'inc';
900             }
901            
902             my $text = $w->get( $start, $end );
903             $w->delete( $start, $end );
904            
905             if( not defined $r ) {
906             # With default register, d shifts
907             # XXX: can you not get a hash slice with references?
908             for my $idx ( 2 .. 9 ) {
909             $w->{VI_REGISTER}{ $idx } = $w->{VI_REGISTER}{ $idx-1 };
910             }
911            
912             # Stores in "1 by default
913             $r = '1';
914            
915             # If under 1 line, store in small delete register too
916             $w->registerStore( '-', $text ) if $text !~ /\n/;
917             }
918            
919             $w->registerStore( $r, $text );
920             }
921            
922             sub vi_n_f {
923             my ($w,$k,$n,$r,$m) = @_;
924            
925             die X_NO_KEYS if $k eq '';
926            
927             my $line = $w->get( 'insert', 'insert lineend' );
928             my $ofst = index $line, $k, 1;
929             for (2 .. $n) {
930             return if $ofst == -1;
931             $ofst = index $line, $k, $ofst+1;
932             }
933            
934             return if $ofst == -1;
935             return [ "insert +$ofst c", 'char', 'inc' ];
936             }
937            
938             sub vi_n_ga {
939             my ($w,$k,$n,$r,$m) = @_;
940             die X_BAD_STATE if $m;
941            
942             my $c = $w->get( 'insert' );
943             my $sc = $c;
944             if( ord($c) < 0x20 ) {
945             $sc = '^' . chr( ord($c) + 64 );
946             }
947            
948             $w->setMessage(sprintf '<%s> %d, Hex %02x, Oct %03o', $sc, (ord($c)) x 3 );
949             }
950            
951             sub vi_n_gg {
952             my ($w,$k,$n,$r,$m) = @_;
953            
954             return [ "$n.0", 'line' ];
955             }
956            
957             sub vi_n_h {
958             my ($w,$k,$n,$r,$m) = @_;
959             $n ||= 1;
960            
961             my $ind = ( split /\./, $w->index('insert') )[1];
962             return [ 'insert linestart', 'char', 'exc' ] if $ind <= $n;
963             return [ "insert -$n c", 'char', 'exc' ];
964             }
965            
966             sub vi_n_i {
967             my ($w,$k,$n,$r,$m) = @_;
968             $w->viMode('i');
969             $w->addGlobStart;
970             }
971            
972             sub vi_n_j {
973             my ($w,$k,$n,$r,$m) = @_;
974             $n ||= 1;
975            
976             # Screwy, Setcursor('end') doesn't make index('insert') == index('end')??
977             my $max = int $w->index('end') - 1 - int $w->index('insert');
978             $n = $max if $n > $max;
979            
980             return if $n == 0;
981             [ "insert +$n l", 'line', 'inc' ];
982             }
983            
984             sub vi_n_k {
985             my ($w,$k,$n,$r,$m) = @_;
986             $n ||= 1;
987            
988             my $max = int $w->index('insert') - 1;
989             $n = $max if $n > $max;
990            
991             return if $n == 0;
992             [ "insert -$n l", 'line', 'inc' ];
993             }
994            
995             sub vi_n_l {
996             my ($w,$k,$n,$r,$m) = @_;
997            
998             $n ||= 1;
999             my $ln = $w->index('insert lineend');
1000             my $ln_1 = $w->index('insert lineend -1c');
1001             my $eol = (split /\./, $ln_1)[1];
1002             my $ins = $w->index('insert');
1003             my $at = (split /\./, $ins)[1];
1004            
1005             # If the cursor is at TK's end of line or VI end of line, leave it alone
1006             return ['insert','char','exc'] if $ln eq $ins or $ln_1 eq $ins;
1007             # If the count would go past lineend - 1, stop at lineend - 1
1008             return ['insert lineend -1c','char','inc'] if $n + $at >= $eol;
1009             # Otherwise advance n characters
1010             return ["insert +$n c",'char','exc'];
1011             }
1012            
1013             sub vi_n_m {
1014             my ($w,$k,$n,$r,$m) = @_;
1015             die X_BAD_STATE if $m;
1016             die X_NO_KEYS if $k eq '';
1017            
1018             $w->markSet( "VI_MARK_$k", 'insert' );
1019             }
1020            
1021             sub vi_n_n {
1022             my ($w,$k,$n,$r,$m) = @_;
1023            
1024             my $re = $w->{VI_SEARCH_LAST};
1025            
1026             if( not defined $re ) {
1027             $w->setError('No pattern');
1028             die X_BAD_STATE;
1029             }
1030            
1031             my $text = $w->get( 'insert +1c', 'end' );
1032            
1033             if( $text =~ $re ) {
1034             return [ "insert +1c +$-[0]c", 'char', 'exc' ];
1035             }
1036             }
1037            
1038             sub vi_n_o {
1039             my ($w,$k,$n,$r,$m) = @_;
1040             die X_NO_MOTION if $m;
1041            
1042             # Work around for some weird behavior in Tk::TextUndo
1043             # If I just open the line and advance the cursor, I lose
1044             # test case 6
1045             my ($l) = 1 + int $w->index('insert');
1046             $w->insert('insert lineend',"\n");
1047             $w->SetCursor("$l.0");
1048             $w->viMode('i');
1049             }
1050            
1051             sub vi_n_p {
1052             my ($w,$k,$n,$r,$m) = @_;
1053             die X_BAD_STATE if $m;
1054            
1055             $r = "" if not defined $r;
1056             $n ||= 1;
1057            
1058             my $txt = $w->registerGet($r);
1059            
1060             if( index( $txt, "\n" ) == -1 ) {
1061             # Charwise insert
1062             $w->insert( 'insert +1c', $txt x $n );
1063             $n *= length($txt);
1064             $n += 1;
1065             $w->SetCursor( "insert +$n c" );
1066             }
1067             else {
1068             # Linewise insert
1069             $w->insert( 'insert +1l linestart', $txt x $n );
1070             $w->SetCursor( 'insert +1l linestart' );
1071             }
1072             }
1073            
1074             sub vi_n_q {
1075             my ($w,$k,$n,$r,$m) = @_;
1076             die X_NO_MOTION if $m;
1077            
1078             # Completed a mapping
1079             if( defined $w->{VI_RECORD_REGISTER} ) {
1080             # Remove this 'q'
1081             chop $w->{VI_RECORD_KEYS};
1082             $w->{VI_REGISTER}{ $w->{VI_RECORD_REGISTER} } = $w->{VI_RECORD_KEYS};
1083             $w->{VI_RECORD_REGISTER} = undef;
1084             }
1085             else {
1086             die X_NO_KEYS if $k eq '';
1087             die X_BAD_STATE if $k =~ /[_:.%#]/;
1088            
1089             $w->{VI_RECORD_KEYS} = '';
1090             $w->{VI_RECORD_REGISTER} = $k;
1091             }
1092             }
1093            
1094             sub vi_n_r {
1095             my ($w,$k,$n,$r,$m) = @_;
1096             die X_NO_MOTION if $m;
1097             die X_NO_KEYS if $k eq '';
1098            
1099             $n ||= 1;
1100             die X_BAD_STATE if $w->compare("insert +$n c",'>','insert lineend');
1101            
1102             if( uc $w->{VI_MODE} eq 'V' ) {
1103             my $start = $w->index('sel.first');
1104             my $text = $w->get( $start, 'sel.last' );
1105             $text =~ s/./$k/g; # no /s newlines stay intact!
1106            
1107             # Save idx, about to delete the selection
1108             my $idx = $w->index( 'sel.first' );
1109            
1110             $w->delete( $start, 'sel.last' );
1111             $w->insert( $start, $text );
1112             $w->SetCursor( $idx );
1113             }
1114             else {
1115             # Grrr. Tk::Text moves the mark when I want to insert after it.
1116             my $pos = $w->index('insert');
1117             $w->delete('insert', "insert +$n c");
1118             $w->insert('insert',$k x $n);
1119             $w->SetCursor( $pos );
1120             }
1121             }
1122            
1123             sub vi_n_t {
1124             my ($w,$k,$n,$r,$m) = @_;
1125            
1126             die X_NO_KEYS if $k eq '';
1127            
1128             my $line = $w->get( 'insert', 'insert lineend' );
1129             my $ofst = index $line, $k, 1;
1130             for (2 .. $n) {
1131             return if $ofst == -1;
1132             $ofst = index $line, $k, $ofst+1;
1133             }
1134            
1135             return if $ofst == -1;
1136             return [ "insert +$ofst c -1c", 'char', 'inc' ];
1137             }
1138            
1139             sub vi_n_u {
1140             my ($w,$k,$n,$r,$m) = @_;
1141             $w->undo;
1142             }
1143            
1144             sub vi_n_v {
1145             my ($w,$k,$n,$r,$m) = @_;
1146             die X_BAD_STATE if $m;
1147             $w->viMode('v');
1148             $w->{VI_VISUAL_START} = $w->index('insert');
1149             return ['insert','char','inc'];
1150             }
1151            
1152             sub vi_n_y {
1153             my ($w,$k,$n,$r,$m) = @_;
1154             my($start,$end,$wise,$type);
1155             die X_BAD_STATE if $m;
1156            
1157             # In a visual mode we just need the selection
1158             if( $w->{VI_MODE} eq 'v' ) {
1159             $start = 'sel.first';
1160             $end = 'sel.last';
1161             $wise = 'char';
1162             $type = 'exc';
1163             }
1164             elsif( $w->{VI_MODE} eq 'V' ) {
1165             $start = 'sel.first';
1166             $end = 'sel.last';
1167             $wise = 'line';
1168             }
1169             # In normal mode there's more work
1170             else {
1171             # Special case, dd = delete line
1172             if( $k eq 'y' ) {
1173             $start = 'insert';
1174             $end = 'insert';
1175             $end .= '+' . ($n-1) . 'l' if $n > 1;
1176             $wise = 'line';
1177             }
1178             else {
1179             my $res = EvalKeys( @_[0 .. 3], 1 );
1180            
1181             $start = 'insert';
1182             ($end,$wise,$type) = @$res;
1183             }
1184             }
1185            
1186             # Swap start and end if the motion was backwards
1187             if( $w->compare( $start, '>', $end ) ) {
1188             ($start,$end) = ($end,$start);
1189             $type = 'exc'; # XXX: hack
1190             }
1191            
1192             if( $wise eq 'line' ) {
1193            
1194             $start .= ' linestart'; # From start of line
1195             $end .= ' lineend +1c'; # Including the \n of the final line
1196             }
1197             else {
1198             $end .= ' +1c' if $type eq 'inc';
1199             }
1200            
1201             my $text = $w->get( $start, $end );
1202            
1203             if( not defined $r ) {
1204             $r = '';
1205             }
1206            
1207             $w->registerStore( $r, $text );
1208             }
1209            
1210             sub vi_n_B {
1211             my ($w,$k,$n,$r,$m) = @_;
1212             $n ||= 1;
1213            
1214             my ($row,$col) = split /\./, $w->index('insert');
1215             my $line = $w->get( 'insert linestart', 'insert lineend' );
1216             while( $n > 0 ) {
1217             # Check for back one word on this line
1218             if( substr($line,0,$col) =~ /\S+\s*$/ ) {
1219             $n--;
1220             $col = $-[0];
1221             }
1222             else {
1223             return [ '1.0', 'char', 'inc' ] if $row == 1;
1224             $row--;
1225             $line = $w->get( "$row.0", "$row.0 lineend" );
1226             }
1227             }
1228             return [ "$row.$col", 'char', 'inc' ];
1229             }
1230            
1231             sub vi_n_E {
1232             my ($w,$k,$n,$reg,$m) = @_;
1233             my $l;
1234             $n ||= 1;
1235            
1236             my $ofst = 0;
1237             my ($r,$c) = split /\./, $w->index('insert');
1238             my ($maxr,$maxc) = split /\./, $w->index('end');
1239            
1240             my $line = $w->get( 'insert linestart', 'insert lineend' );
1241             pos($line) = $c;
1242             while( $n > 0 ) {
1243             # | abc
1244             # a|bc
1245             # |c def
1246             if( $line =~ /\G.\s*\S*(?=\S)/gc ) { $n--; next; }
1247            
1248             $r++;
1249            
1250             # Can't go past end
1251             if( $r > $maxr ) {
1252             $r = $maxr;
1253             $c = $maxc;
1254             last;
1255             }
1256            
1257             $line = $w->get( "$r.0", "$r.0 lineend" );
1258            
1259             # Catches cases of 1-letter word at start of line
1260             if( $line =~ /^\s*\S*(?=\S)/gc ) { $n--; }
1261             }
1262            
1263             $c = pos($line) || 0;
1264             return [ "$r.$c", "char", "exc" ];
1265             }
1266            
1267             sub vi_n_G {
1268             my ($w,$k,$n,$r,$m) = @_;
1269            
1270             return [ "$n.0", 'line' ] if $n;
1271             return [ 'end -1l linestart', 'line' ];
1272             }
1273            
1274             sub vi_n_I {
1275             my ($w,$k,$n,$r,$m) = @_;
1276             die X_BAD_STATE if $m;
1277            
1278             # Skip cursor over initial blanks
1279             my $line = $w->get( 'insert linestart', 'insert lineend' );
1280             $line =~ /^(\s*)/;
1281             $w->SetCursor( "insert +" . length($1) . "c" );
1282            
1283             $w->viMode('i');
1284             }
1285            
1286             sub vi_n_O {
1287             my ($w,$k,$n,$r,$m) = @_;
1288             die X_NO_MOTION if $m;
1289             $w->insert('insert linestart',"\n");
1290             $w->SetCursor('insert -1l');
1291             $w->viMode('i');
1292             }
1293            
1294             sub vi_n_R {
1295             my ($w,$k,$n,$r,$m) = @_;
1296             die X_NO_MOTION if $m;
1297             $w->viMode('R');
1298             }
1299            
1300             sub vi_n_V {
1301             my ($w,$k,$n,$r,$m) = @_;
1302             die X_BAD_STATE if $m;
1303            
1304             $w->viMode('V');
1305             $w->{VI_VISUAL_START} = $w->index('insert');
1306             #$w->tagAdd( 'sel', 'insert linestart', 'insert lineend' );
1307             return ['insert'];
1308             }
1309            
1310             sub vi_n_W {
1311             my ($w,$k,$n,$reg,$m) = @_;
1312             my $l;
1313             $n ||= 1;
1314            
1315             my $ofst = 0;
1316             my ($r,$c) = split /\./, $w->index('insert');
1317             my ($maxr,$maxc) = split /\./, $w->index('end');
1318            
1319             my $line = $w->get( 'insert linestart', 'insert lineend' );
1320             pos($line) = $c;
1321             while( $n > 0 ) {
1322             # If there is another word on this line
1323             if( $line =~ /\G\S*\s+(?=\S)/gc ) { $n--; next; }
1324            
1325             # Get the next line
1326             $r++;
1327            
1328             # Can't go past end
1329             if( $r > $maxr ) {
1330             $r = $maxr;
1331             $c = $maxc;
1332             last;
1333             }
1334            
1335             $line = $w->get( "$r.0", "$r.0 lineend" );
1336             # Skip leading whitespace
1337             if( $line =~ /^\s+/gc ) {
1338             # Only counts as a word if there is a non-whitespace letter
1339             $n-- if $line =~ /\G\S/gc;
1340             }
1341             else {
1342             # No leading whitespace, either empty line or start of word
1343             # Both count as a word
1344             $n--;
1345             }
1346             }
1347            
1348             $c = pos($line) || 0;
1349             return [ "$r.$c", "char", "exc" ];
1350             }
1351            
1352             sub vi_n_tilde {
1353             my ($w,$k,$n,$r,$m) = @_;
1354            
1355             die X_BAD_STATE if $m;
1356            
1357             my ($start,$end,$chars);
1358            
1359             if( $w->{VI_MODE} =~ /v/i ) {
1360             $start = 'sel.first';
1361             $end = 'sel.last';
1362             }
1363             else {
1364             $start = 'insert';
1365             $end = 'insert +1c';
1366             }
1367            
1368             $start = $w->index( $start ); # save absolute position of start
1369             $end = $w->index( $end );
1370            
1371             $chars = $w->get( $start, $end );
1372             $w->delete( $start, $end );
1373            
1374             # The world was a whole lot simpler before unicode...
1375             $chars =~ s/([[:upper:]])|([[:lower:]])/defined $1 ? lc $1 : uc $2/ge;
1376            
1377             $w->insert( $start, $chars );
1378             }
1379            
1380             sub vi_n_backtick {
1381             my ($w,$k,$n,$r,$m) = @_;
1382            
1383             die X_NO_KEYS if $k eq '';
1384            
1385             $k = $names{$k} || $k;
1386             return unless $w->markExists( "VI_MARK_$k" );
1387            
1388             return [ "VI_MARK_$k", 'char', 'exc' ];
1389             }
1390            
1391             sub vi_n_at {
1392             my ($w,$k,$n,$r,$m) = @_;
1393             $n ||= 1;
1394            
1395             die X_NO_MOTION if $m;
1396             die X_NO_KEYS if $k eq '';
1397            
1398             my $keys = $w->registerGet( $k );
1399             die X_BAD_STATE unless defined $keys;
1400            
1401             my @keys = split //, $keys;
1402            
1403             $w->{VI_PENDING} = '';
1404             local $_;
1405             while( $n > 0 ) {
1406             $n--;
1407             $w->InsertKeypress($_) for @keys;
1408             }
1409             }
1410            
1411             sub vi_n_dollar {
1412             my ($w,$k,$n,$r,$m) = @_;
1413            
1414             $n ||= 1;
1415             $n--;
1416            
1417             my $i0 = $w->index( "insert +$n l lineend" );
1418             # Special case, blank line
1419             return [ "insert +$n l", 'char', 'exc' ] if $i0 =~ /\.0$/;
1420             return [ "insert +$n l lineend -1c", 'char', 'inc' ];
1421             }
1422            
1423             # All the things a % can match
1424             my %brace_left = qw" ( ) { } [ ] ";
1425             my %brace_right = qw" ) ( } { ] [ ";
1426             my $brace_re = join '|', map quotemeta, %brace_left;
1427             $brace_re = qr/($brace_re)/;
1428            
1429             sub vi_n_percent {
1430             my ($w,$k,$n,$r,$m) = @_;
1431            
1432             # If passed a count, goes to % in file instead
1433             if( $n != 0 ) {
1434             return if $n > 100;
1435             my $line = int $w->index('end');
1436             $line *= $n / 100.0;
1437             $line = (int $line) || 1;
1438             return [ "$line.0", 'line' ];
1439             }
1440            
1441             # Find the first bracket-like char on the line after the cursor
1442             my $line = $w->get( 'insert', 'insert lineend' );
1443             return unless( $line =~ $brace_re );
1444             my $brace = $1;
1445             my $ofst = "insert + $-[0] c";
1446            
1447             # Only care about matching up this brace pair
1448             # Don't worry about constructs like ( { )
1449             my $match;
1450             my $dir;
1451             my $count = 0;
1452             my $open = 1;
1453             if( exists $brace_left{$brace} ) {
1454             $match = $brace_left{$brace};
1455             $dir = '+';
1456             }
1457             else {
1458             $match = $brace_right{$brace};
1459             $dir = '-';
1460             }
1461            
1462             while( $open ) {
1463             $count++;
1464             my $char = $w->get( "$ofst $dir $count c" );
1465             $open++ if( $char eq $brace );
1466             $open-- if( $char eq $match );
1467            
1468             # XXX: Yuck. Tk::Text doesn't give us an undef or an error if
1469             # the index is outside the body of the text, it just gives the first
1470             # or last index. This algorithm should really be changed to
1471             # a linewise one because this is #### inefficient.
1472             return if $open && $w->compare( "$ofst $dir $count c", '==', '1.0' );
1473             return if $open && $char eq '' ;
1474             }
1475            
1476             # XXX: I think % becomes linewise if we crossed a \n
1477             return [ "$ofst $dir $count c", "char", "inc" ];
1478             }
1479            
1480            
1481             sub vi_n_colon {
1482             my ($w,$k,$n,$r,$m) = @_;
1483             die X_BAD_STATE if $m;
1484            
1485             my $old = $w->viMode('c');
1486             if( 'v' eq lc $old ) {
1487             $w->{VI_PENDING} = "'<,'>";
1488             }
1489             elsif( $n ) {
1490             $n--;
1491             $w->{VI_PENDING} = $n ? ".,.+$n" : '.';
1492             }
1493            
1494             return ['insert'];
1495             }
1496            
1497             sub vi_n_fslash {
1498             my ($w,$k,$n,$r,$m) = @_;
1499            
1500             # Remember the current location.
1501             $w->{VI_SAVE_CURSOR} = $w->index('insert');
1502            
1503             # Switch to incremental search mode
1504             $w->viMode('/');
1505             }
1506            
1507             sub vi_fslash {
1508             my ($w) = @_;
1509            
1510             $w->tagRemove( 'VI_SEARCH', '1.0', 'end' );
1511            
1512             my $re = eval { qr/$w->{VI_PENDING}/ };
1513            
1514             # Regex is incomplete
1515             return [ $w->{VI_SAVE_CURSOR} ] if $@;
1516            
1517             # XXX: OUCH! maybe we could scan the regex for \n and
1518             # (?s) sequences and scan line by line instead?
1519             my $text = $w->get( $w->{VI_SAVE_CURSOR}, 'end' );
1520             if( $text =~ $re ) {
1521             $w->tagAdd( 'VI_SEARCH', "$w->{VI_SAVE_CURSOR} + $-[0] c", "$w->{VI_SAVE_CURSOR} + $+[0] c" );
1522             return [ "$w->{VI_SAVE_CURSOR} + $-[0] c" ];
1523             }
1524             else {
1525             return [ $w->{VI_SAVE_CURSOR} ];
1526             }
1527             }
1528            
1529             sub vi_fslash_end {
1530             my ($w) = @_;
1531            
1532             $w->tagRemove( 'VI_SEARCH', '1.0', 'end' );
1533            
1534             my $re = eval { qr/$w->{VI_PENDING}/ };
1535            
1536             # Regex is incomplete
1537             return if $@;
1538            
1539             # XXX: OUCH! maybe we could scan the regex for \n and
1540             # (?s) sequences and scan line by line instead?
1541             my $text = $w->get( '1.0', 'end' );
1542             while( $text =~ /$re/g ) {
1543             $w->tagAdd( 'VI_SEARCH', "1.0 + $-[0] c", "1.0 + $+[0] c" );
1544             }
1545            
1546             $w->{VI_SEARCH_LAST} = $re;
1547             }
1548            
1549             # COMMAND MODE ###########################################################
1550            
1551             =begin comment
1552            
1553             sub vi_c_ {
1554             my ($w,$force,$arg) = @_;
1555             }
1556            
1557             =end comment
1558            
1559             =cut
1560            
1561             sub vi_c_none {
1562             my ($w,$force,$arg,$range) = @_;
1563            
1564             if( $force ) {
1565             return unless 2 == scalar @$range;
1566            
1567             my $s = $w->index($range->[0] . ' linestart');
1568             my $e = $w->index($range->[1] . ' lineend' );
1569            
1570             my $res = $w->vi_c( '!', 1, $arg, [ $s, $e ] );
1571             return unless defined $res;
1572            
1573             $w->delete( $s, $e );
1574             $w->insert( $s, $res );
1575             }
1576             else {
1577             return unless @$range;
1578             $w->SetCursor( pop @$range );
1579             }
1580             }
1581            
1582             sub vi_c_map {
1583             my ($w,$force,$arg) = @_;
1584            
1585             my ($seq,$cmd) = split / +/, $arg, 2;
1586            
1587             $w->viMap( 'nv', $seq, \$cmd ) or $w->setError( 'Ambiguous mapping' );
1588             }
1589            
1590             sub vi_c_nohlsearch {
1591             my ($w,$force,$arg) = @_;
1592            
1593             $w->tagRemove( 'VI_SEARCH', '1.0', 'end' );
1594             }
1595            
1596             sub vi_c_set {
1597             my ($w,$force,$arg) = @_;
1598            
1599             if( $arg =~ /^\s*(\w+)\?$/ ) {
1600             my $key = $1;
1601             my $value = $w->settingGet( $key );
1602             $w->setMessage( "$key=$value" );
1603             }
1604             elsif( $arg =~ /^\s*(\w+)[=:](.*)/ ) {
1605             my ($key,$value) = ($1,$2);
1606             if( not exists $settings{$key} ) {
1607             $w->setError( "Setting '$key' does not exist." );
1608             return;
1609             }
1610            
1611             $key = ${$settings{$key}} if 'SCALAR' eq ref $settings{$key};
1612            
1613             $value += 0 if $settings{$key}[2] eq 'int';
1614             $settings{$key}[0] = $value;
1615             }
1616             }
1617            
1618             sub vi_c_split {
1619             my ($w,$force,$arg) = @_;
1620            
1621             my $newwin = $w->vi_c( 'split' );
1622             return if not defined $newwin;
1623            
1624             if( ref $newwin ) {
1625             $w->vi_split( $newwin );
1626             }
1627             else {
1628             $w->setError( $newwin );
1629             }
1630             }
1631            
1632             1;
1633            
1634             =head1 NAME
1635            
1636             Tk::TextVi - Tk::Text widget with Vi-like commands
1637            
1638             =head1 SYNOPSIS
1639            
1640             use Tk::TextVi;
1641            
1642             $textvi = $window->TextVi( -option => value, ... );
1643            
1644             =head1 DESCRIPTION
1645            
1646             Tk::TextVi is a Tk::TextUndo widget that replaces InsertKeypress() to handle user input similar to vi. All other methods remain the same (and most code should be using $text->insert( ... ) rather than $text->InsertKeypress()). This only implements the text widget and key press logic; the status bar must be drawn by the application (see TextViDemo.pl for an example of this).
1647            
1648             To use Tk::TextVi as a drop-in replacement for other text widgets, see Tk::EditorVi which encapsulates Tk::TextVi and the status bar into a composite widget. (This module is included in the Tk::TextVi distribution, but is not installed.)
1649            
1650             Functions in Vi that require interaction with the system (such as reading or writing files) are not (currently) handled by this module (This is a feature since you probably don't want :quit to do exactly that). Instead a callback is provided so that the application using the Tk::TextVi widget may decide how to act on them.
1651            
1652             The cursor in a Tk::Text widget is a mark placed between two characters. Vi's idea of a cursor is placed on a non-newline character or a blank line. Tk::TextVi treats the cursor as on (in the Vi-sense) the characters following the cursor (in the Tk::Text sense). This means that $ will place the cursor just before the final character on the line.
1653            
1654             =head2 Options
1655            
1656             =over 4
1657            
1658             =item -statuscommand
1659            
1660             Callback invoked when the mode or the keys in the pending command change. The current mode and pending keys will be passed to this function.
1661            
1662             =item -messagecommand
1663            
1664             Callback invoked when messages need to be displayed.
1665            
1666             =item -errorcommand
1667            
1668             Callback invoked when error messages need to be displayed.
1669            
1670             =item -commands
1671            
1672             Stores callbacks to handle command-mode commands which require external action. See the commands() method below for more details.
1673            
1674             =back
1675            
1676             =head2 Methods
1677            
1678             All methods present in Tk::Text and Tk::TextUndo are inherited by Tk::TextVi. Additional or overridden methods are as follows:
1679            
1680             =over 4
1681            
1682             =item $text->InsertKeypress( $char );
1683            
1684             This replaces InsertKeypress() in Tk::Text to recognise vi commands.
1685            
1686             =item $text->SetCursor( $index );
1687            
1688             This replaces SetCursor() in Tk::Text with one that is aware of the visual selection.
1689            
1690             =item $text->viMode( $mode );
1691            
1692             Returns the current mode of the widget:
1693            
1694             'i' # insert
1695             'n' # normal
1696             'c' # command
1697             'R' # replace
1698             'v' # visual character
1699             'V' # visual line
1700            
1701             There is also a fake mode:
1702            
1703             '/' # Incremental search
1704            
1705             If the 'q' command (record macro) is currently active, a q will be appended to the mode.
1706            
1707             The insert-XXX modes (entered by Control-O from insert mode) are indicated by a two-character sequence, 'i' followed by the character of the mode that is active. (e.g. 'in' is insert-normal).
1708            
1709             If the $mode parameter is supplied, it will set the mode as well. Any pending keystrokes will be cleared (this brings the widget to a known state). Macro-recording or incremental search cannot be enabled from this function.
1710            
1711             =item $text->viPending;
1712            
1713             Returns the current buffer of pending keystrokes. In normal or visual mode this is the pending command, in command mode this is the partial command line.
1714            
1715             =item $text->viError;
1716            
1717             Returns a list of all pending error messages.
1718            
1719             =item $text->viMessage;
1720            
1721             Returns a list of all pending non-error messages (for example the result of normal-ga)
1722            
1723             =item $text->viMap( $mode, $sequence, $ref, $force )
1724            
1725             $mode should be one of qw( n c v ) for normal, command and visual mode respectively. Mappings are shared between the different visual modes. $sequence is the keypress sequence to map the action to. To map another sequence of keys to be interpreted by Tk::TextVi as keystrokes, pass a scalar reference. A code reference will be called by Tk::Text (the signature of the function is described below). A hash reference can be used to restore several mappings (as described below). If $ref is the empty string the current mapping is deleted.
1726            
1727             The function may fail--returning undef--in two cases:
1728            
1729             =over 4
1730            
1731             =item *
1732            
1733             You attempt to map to a sequence that begins another command (for example you cannot map to 'g' since there is a 'ga' command). Setting $force to a true value will force the mapping and will remove all other mappings that begin with that sequence.
1734            
1735             =item *
1736            
1737             You attempt to map to a sequence that starts with an existing command (for example, you cannot map to 'aa' since there is an 'a' command). Setting $force to a true value will remove the mapping that conflicts with the requested sequence.
1738            
1739             =back
1740            
1741             =item $w->commands( $key => $sub, $key2 => $sub2 )
1742             =item $w->commands( { $key => $sub } )
1743            
1744             Sets the commands configuration setting. In the first form, the given commands are updated with the listed commands. Passing 'undef' for a subroutine will remove that entry. In the second form, the passed hashref replaces the current command list. The subroutine associated with the key 'NOT_SUPPORTED' is called when a typed command is not found in the hash.
1745            
1746             Each callback receives arguments of the form:
1747            
1748             my ( $textvi, $force, $args, $range ) = @_;
1749            
1750             Where $textvi is the Tk::TextVi instance, $force is a true value if the command was followed by an exclaimation point, $args is any text following the command and $range is an arrayref giving any lines entered before the command. The elements of this array are valid input to the Tk::Text->index() method.
1751            
1752             The NOT_SUPPORTED callback takes an additional argument containing the typed command:
1753            
1754             my ( $textvi, $cmd, $force, $args, $range ) = @_;
1755            
1756             The following commands are defined by TextVi and may take different arguments than the above:
1757            
1758             =over
1759            
1760             =item split
1761            
1762             Called for the :split command. The callback should return a Tk::TextVi instance to use as the newly created window. See EXPERIMENTAL FEATURES below for more details. None of the arguments are currently meaningful.
1763            
1764             =item !
1765            
1766             Called for the :! (filter) command. $args is the command line and $range gives the lines to process ($textvi->get( @$range ) will return the text to be filtered). The $force argument is not meaningful.
1767            
1768             The callback should return the text filtered through the given program specified, or undef if the text should not be modified (either due to an error executing the command, or the callback has updated the widget itself).
1769            
1770             =back
1771            
1772             All commands listed in the implemented commands that are not listed above are implemented internally and ignore these callbacks.
1773            
1774             =back
1775            
1776             =head2 Bindings
1777            
1778             The bindings present in Tk::Text are inherited by Tk::TextVi, however it is not safe to rely on the control key bindings since many of these are used by vi.
1779            
1780             =head1 SETTINGS
1781            
1782             =over
1783            
1784             =item softtabstop
1785            
1786             =item sts
1787            
1788             This setting is a combination of the softtabstop and expandtab found in Vi/Vim. Setting it to a non-zero value has the following effects: The backspace key will delete spaces to reach a column number that is an even multiple of the softtabstop value; the tab key will insert places to reach the next column that is an even multiple of the softtabstop value. When set to zero, backspace always deletes one character and tab inserts a literal tab. (default value is 4)
1789            
1790             =back
1791            
1792             =head1 COMMANDS
1793            
1794             =head2 Supported Commands
1795            
1796             =head3 Insert Mode
1797            
1798             Keypresses in insert mode are added to the text literally except for the
1799             following special keys.
1800            
1801             Tab Insert spaces up to the next softtabstop
1802             Backspace Delete a character or spaces back to the last softtabstop
1803             Control-O Enter a single normal-mode command
1804            
1805             =head3 Normal Mode
1806            
1807             a - enter insert mode after the current character
1808             d - delete over 'motion' and store in 'register'
1809             dd - delete a line
1810             f - find next occurrence of 'character' on this line
1811             g - one of the two-character commands below
1812             ga - print ASCII code of character at cursor
1813             gg - go to the 'count' line
1814             h - left one character on this line
1815             i - enter insert mode
1816             j - down one line
1817             k - up one line
1818             l - right one character on this line
1819             m - set 'mark' at cursor location
1820             n - next occurrance of last match
1821             o - open a line below cursor
1822             p - insert contents of 'register'
1823             q - record keystrokes
1824             r - replace character
1825             t - move one character before the next occurrence of 'character'
1826             u - undo
1827             v - enter visual mode
1828             w - advance one word [1]
1829             x - delete character
1830             y - yank over 'motion' into 'register'
1831             yy - yank a line
1832            
1833             D - delete until end of line
1834             G - jump to 'count' line
1835             O - open line above cursor
1836             R - enter replace mode
1837             V - enter visual line mode
1838             W - advance one word
1839            
1840             ` - move cursor to 'mark'
1841             ~ - toggle case of next character
1842             @ - execute keys from register
1843             $ - go to last character of current line
1844             % - find matching bracketing character
1845             0 - go to start of current line
1846             : - enter command mode
1847             / - search using a regex [2]
1848            
1849             [1] The w command is currently mapped to W
1850             [2] The / command uses a perl regex not the vi or vim syntax
1851            
1852             =head3 Visual Mode
1853            
1854             Normal-mode motion commands will move the end of the visual area. Normal-mode commands that operate over a motion will use the visual selection.
1855            
1856             There are currently no commands defined specific to visual mode.
1857            
1858             =head3 Command Mode
1859            
1860             :
1861             - places the cursor at the last item in range
1862             :map sequence commands
1863             - maps sequence to commands
1864             :noh
1865             :nohl
1866             :nohlsearch
1867             - clear the highlighting from the last search
1868             :set setting?
1869             - prints the value of a setting [1]
1870             :set setting=value
1871             - set the value of a setting
1872             :split
1873             - split the window
1874             :!program
1875             - filter text through program
1876            
1877             [1] :set does not display a setting's value as a result of the command
1878             :set non-bool-setting. The final ? must be supplied.
1879            
1880             Commands may have a ! suffix to force completion (e.g. :map! with map a command even if it will overwrite existing mappings). They may also be prefixed with a range of text to operate on:
1881            
1882             . # The current line number
1883             $ # The final line
1884             % # The entire text, same as 1,$
1885             NUM # Line number NUM
1886             'x # The line containing mark x
1887             RANGE+NUM # NUM lines after RANGE
1888            
1889             Multiple values may be separated with , or ; (Tk::TextVi does not currently distinguish between the delimiters).
1890            
1891             =head2 EXPERIMENTAL COMMANDS
1892            
1893             =head3 :split
1894            
1895             First, :split is only included as a "look at this cool feature" do not count on it to work the same way in the future, or work at all now. It doesn't even support the ":split file" syntax. The current implementation is a bit memory-intensive and slows many basic methods of the Tk::Text widget (don't use :split and you won't get penalized).
1896            
1897             Second, none of the supporting commands are implemented. :quit will not close only one window, and there are no Normal-^W commands.
1898            
1899             The split callback should return a new Tk::TextVi widget to the caller or a string to be used as an error message. The module will copy the contents and make sure all changes in the text are visible in both widgets.
1900            
1901             =head2 WRITING COMMANDS
1902            
1903             Perl subroutines can be mapped to keystrokes using the viMap() method described above. Normal and visual mode commands receive arguments like:
1904            
1905             my ( $widget, $keys, $count, $register, $wantmotion ) = @_;
1906            
1907             Where $widget is the Tk::TextVi object, $keys are any key presses entered after those that triggered the function. Unless you've raised X_NO_KEYS this should be an empty string. $count is the current count, zero if none has been set. $register contains the name of the entered register. $wantmotion will be a true value if this command is being called in a context that requires a cursor motion (such as from a d command).
1908            
1909             Commands receive arguments in the following format:
1910            
1911             my ( $widget, $forced, $argument, $range ) = @_;
1912            
1913             $forced is set to a true value if the command ended with an exclamation point. $argument is set to anything that comes after the command. $range is an array reference that contains any line numbers removed from the front of the command. The elements are valid input to Tk::Text::index.
1914            
1915             To move the cursor a normal-mode command should return an array reference. The first parameter is a string representing the new character position in a format suitable to Tk::Text. The second is either 'line' or 'char' to specify line-wise or character-wise motion. Character-wise motion should also specify 'inc' or 'exc' for inclusive or exclusive motion as the third parameter.
1916            
1917             Scalar references will be treated as a sequence of keys to process. All other return values will be ignored, but avoid returning references (any future expansion will use leave plain scalar returns alone).
1918            
1919             =head3 Exceptions
1920            
1921             =over 4
1922            
1923             =item X_NO_MOTION
1924            
1925             If a true value is passed for $wantmotion and the function is not a motion command, die with this value.
1926            
1927             =item X_NO_KEYS
1928            
1929             Use when additional key presses are required to complete the command.
1930            
1931             =item X_BAD_STATE
1932            
1933             For when the command can't complete and panic is more appropriate than doing nothing.
1934            
1935             =back
1936            
1937             =head3 Methods
1938            
1939             =over 4
1940            
1941             =item $text->EvalKeys( $keys, $count, $register, $wantmotion )
1942            
1943             Uses keys to determine the function to call passing it the count, register and wantmotion parameters specified. The return value will be whatever that function returns. If wantmotion is a true value the return value will always be an array reference as described above.
1944            
1945             Normally you want to call this function like this, passing in the set of keystrokes after the command, the current count, the current register and setting wantmotion to true:
1946            
1947             $w->EvalKeys( @_[1..3], 1 )
1948            
1949             =item $text->setMessage( $msg )
1950            
1951             Queue a message to be displayed and generate the associated event.
1952            
1953             =item $text->setError( $msg )
1954            
1955             Same as setMessage, but the message is added to the error list and the error message event is generated.
1956            
1957             =item $text->registerStore( $register, $text )
1958            
1959             Store the contents of $text into the specified register. The text will also be stored in the unnamed register. If the '*' register is specified, the clipboard will be used. If the black-hole or a read-only register is specified nothing will happen.
1960            
1961             =item $text->registerGet( $register )
1962            
1963             Returns the text stored in a register
1964            
1965             =item $text->settingGet( $setting )
1966            
1967             Returns the value of the specified setting.
1968            
1969             =back
1970            
1971             =head1 BUGS AND CAVEATS
1972            
1973             If you find a bug in the handling of a vi-command, please try to produce an example that looks something like this:
1974            
1975             $text->Contents( <
1976             Some
1977             Initial
1978             State
1979             END
1980            
1981             $text->InsertKeypress( $_ ) for split //, 'commands in error';
1982            
1983             Along with the expected final state of the widget (contents, cursor location, register contents etc).
1984            
1985             If the bug relates to the location of the cursor after the command, note the difference between Tk::Text cursor positions and vi cursor positions described above. The command may be correct, but the cursor location looks wrong due to this difference.
1986            
1987             =head2 Known Bugs
1988            
1989             =over 4
1990            
1991             =item *
1992            
1993             Using the mouse or $text->SetCursor you may illegally place the cursor after the last character in the line.
1994            
1995             =item *
1996            
1997             Similarly, movement with the mouse or arrow keys can cause problems when a the state of the widget depends on cursor location.
1998            
1999             =item *
2000            
2001             Counts are not implemented on insert commands like normal-i or normal-o.
2002            
2003             =item *
2004            
2005             Commands that use mappings internally (D and x) do not correctly use the count or registers.
2006            
2007             =item *
2008            
2009             Normal-/ should behave like a motion, but doesn't.
2010            
2011             =item *
2012            
2013             Normal-/ and normal-n will not wrap after hitting end of file.
2014            
2015             =item *
2016            
2017             Normal-u undoes individual Tk::Text actions rather than vi-commands.
2018            
2019             =item *
2020            
2021             Insert-Control-O does not work with visual-mode
2022            
2023             =item *
2024            
2025             Keypresses that map to a movement command do not work as motions.
2026            
2027             =item *
2028            
2029             This modules makes it much easier to commit the programmer's third deadly sin.
2030            
2031             =back
2032            
2033             =head1 SEE ALSO
2034            
2035             Tk::TextUndo and Tk::Text for details on the inherited functionality.
2036            
2037             :help index (in vim) for details on the commands emulated.
2038            
2039             =head1 AUTHOR
2040            
2041             Joseph Strom, C<< >>
2042            
2043             =head1 COPYRIGHT & LICENSE
2044            
2045             Copyright 2008 Joseph Strom, all rights reserved.
2046            
2047             This program is free software; you can redistribute it and/or modify it
2048             under the same terms as Perl itself.
2049