Add ReadLine file completion if no other matches
[p5sagit/Devel-REPL.git] / lib / Devel / REPL / Plugin / Completion.pm
1 package Devel::REPL::Plugin::Completion;
2 use Devel::REPL::Plugin;
3 use Scalar::Util 'weaken';
4 use PPI;
5 use namespace::clean -except => [ 'meta' ];
6
7 has current_matches => (
8   is => 'rw',
9   isa => 'ArrayRef',
10   lazy => 1,
11   default => sub { [] },
12 );
13
14 has match_index => (
15   is => 'rw',
16   isa => 'Int',
17   lazy => 1,
18   default => sub { 0 },
19 );
20
21 has no_term_class_warning => (
22   isa => "Bool",
23   is  => "rw",
24   default => 0,
25 );
26
27 before 'read' => sub {
28   my ($self) = @_;
29
30   if ((!$self->term->isa("Term::ReadLine::Gnu") and !$self->term->isa("Term::ReadLine::Perl"))
31         and !$self->no_term_class_warning) {
32      warn "Term::ReadLine::Gnu or Term::ReadLine::Perl is required for the Completion plugin to work";
33      $self->no_term_class_warning(1);
34   }
35
36   my $weakself = $self;
37   weaken($weakself);
38
39   if ($self->term->isa("Term::ReadLine::Gnu")) {
40      $self->term->Attribs->{attempted_completion_function} = sub {
41         $weakself->_completion(@_);
42      };
43   }
44
45   if ($self->term->isa("Term::ReadLine::Perl")) {
46      $self->term->Attribs->{completion_function} = sub {
47         $weakself->_completion(@_);
48      };
49   }
50
51 };
52
53 sub _completion {
54    my $is_trp = scalar(@_) == 4 ? 1 : 0;
55    my ($self, $text, $line, $start, $end) = @_;
56    $end = $start+length($text) if $is_trp;
57
58    # we're discarding everything after the cursor for completion purposes
59    # we can't just use $text because we want all the code before the cursor to
60    # matter, not just the current word
61    substr($line, $end) = '';
62
63    my $document = PPI::Document->new(\$line);
64    return unless defined($document);
65
66    $document->prune('PPI::Token::Whitespace');
67
68    my @matches = $self->complete($text, $document);
69
70    # iterate through the completions
71    if ($is_trp) {
72       if (scalar(@matches)) {
73          return @matches;
74       } else {
75          return readline::rl_filename_list($text);
76       }
77    } else {
78       if (scalar(@matches)) {
79          return $self->term->completion_matches($text, sub {
80                my ($text, $state) = @_;
81
82                if (!$state) {
83                   $self->current_matches(\@matches);
84                   $self->match_index(0);
85                }
86                else {
87                   $self->match_index($self->match_index + 1);
88                }
89
90                return $self->current_matches->[$self->match_index];
91             });
92       } else {
93          # fall back to filename completion for Term::ReadLine::Gnu
94          return;
95       }
96    }
97 }
98
99 sub complete {
100   return ();
101 }
102
103 # recursively find the last element
104 sub last_ppi_element {
105   my ($self, $document, $type) = @_;
106   my $last = $document;
107   while ($last->can('last_element') && defined($last->last_element)) {
108     $last = $last->last_element;
109     return $last if $type && $last->isa($type);
110   }
111   return $last;
112 }
113
114 1;
115
116 __END__
117
118 =head1 NAME
119
120 Devel::REPL::Plugin::Completion - Extensible tab completion
121
122 =head1 AUTHOR
123
124 Shawn M Moore, C<< <sartak at gmail dot com> >>
125
126 =cut
127