Index: perl/lib/BarnOwl.pm
===================================================================
--- perl/lib/BarnOwl.pm	(revision eede1bf40fe1489711da001a16d6ed77fb9aeea5)
+++ perl/lib/BarnOwl.pm	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
@@ -34,4 +34,5 @@
 use BarnOwl::Timer;
 use BarnOwl::Editwin;
+use BarnOwl::Completion;
 
 =head1 NAME
Index: perl/lib/BarnOwl/Completion.pm
===================================================================
--- perl/lib/BarnOwl/Completion.pm	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
+++ perl/lib/BarnOwl/Completion.pm	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
@@ -0,0 +1,20 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+BarnOwl::Completion
+
+=head1 DESCRIPTION
+
+Hooks for tab-completion support in BarnOwl.
+
+=cut
+
+package BarnOwl::Completion;
+
+use BarnOwl::Completion::Context;
+
+
+
+1;
Index: perl/lib/BarnOwl/Completion/Context.pm
===================================================================
--- perl/lib/BarnOwl/Completion/Context.pm	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
+++ perl/lib/BarnOwl/Completion/Context.pm	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
@@ -0,0 +1,131 @@
+use warnings;
+use strict;
+
+=head1 NAME
+
+BarnOwl::Completion::Context
+
+=head1 DESCRIPTION
+
+BarnOwl::Completion::Context is the context that is passed to a
+completion function by BarnOwl. It contains information on the text
+being completed.
+
+=head1 METHODS
+
+=head2 line
+
+The entire command-line currently being completed.
+
+=head2 point
+
+The index of the cursor in C<line>, in the range C<[0,len($line)]>.
+
+=head2 words
+
+The current command-line, tokenized according to BarnOwl's
+tokenization rules, as an array reference.
+
+=head2 word
+
+The current word the point is sitting in, as an index into C<words>.
+
+=head2 word_point
+
+The index of the point within C<$words->[$word]>.
+
+=cut
+
+package BarnOwl::Completion::Context;
+
+use base qw(Class::Accessor::Fast);
+
+__PACKAGE__->mk_ro_accessors(qw(line point words word word_point));
+
+sub new {
+    my $class = shift;
+    my $before_point = shift;
+    my $after_point = shift;
+
+    my $line  = $before_point . $after_point;
+    my $point = length ($before_point);
+    my ($words, $word, $word_point) = tokenize($line, $point);
+
+    my $self = {
+        line  => $line,
+        point => $point,
+        words => $words,
+        word  => $word,
+        word_point => $word_point
+       };
+    return bless($self, $class);
+}
+
+=for doc
+
+Ideally, this should use the same codepath we use to /actually/
+tokenize commands, but for now, make sure this is kept in sync with
+owl_parseline in util.c
+
+Unlike owl_parseline, we always return a result, even in the presence
+of parse errors, since we may be called on incomplete command-lines.
+
+The owl_parseline rules are:
+
+* Tokenize on ' ' and '\t'
+* ' and " are quote characters
+* \ has no effect
+
+=cut
+
+my $boring = qr{[^'" \t]};
+my $quote  = qr{['"]};
+my $space  = qr{[ \t]};
+
+sub tokenize {
+    my $line = shift;
+    my $point = shift;
+
+    my @words = ();
+    my $cword = 0;
+    my $word_point;
+
+    my $word = '';
+    my $skipped = 0;
+
+    pos($line) = 0;
+    while(pos($line) < length($line)) {
+        if($line =~ m{\G ($boring+) }gcx) {
+            $word .= $1;
+        } elsif ($line =~ m{\G ($quote)}gcx) {
+            my $chr = $1;
+            $skipped++ if pos($line) > $point;
+            if($line =~ m{\G ([^$chr]*) $chr}gcx) {
+                $word .= $1;
+                $skipped++ if pos($line) > $point;
+            } else {
+                $word .= substr($line, pos($line));
+                pos($line) = length($line);
+            }
+        }
+
+        if ($line =~ m{\G ($space+|$)}gcx) {
+            my $wend = pos($line) - length($1);
+            push @words, $word;
+            $cword++ unless $wend >= $point;
+            if ($wend >= $point && !defined($word_point)) {
+                $word_point = length($word) - ($wend - $point) + $skipped;
+            }
+            $word = '';
+            $skipped = 0;
+        }
+    }
+
+    if(length($word)) { die("Internal error, leftover=$word"); }
+
+    $word_point = 0 unless defined($word_point);
+
+    return (\@words, $cword, $word_point);
+}
+
+1;
Index: t/completion.t
===================================================================
--- t/completion.t	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
+++ t/completion.t	(revision 8eac1a5be01a70875c0d351ef12d9ab5f2cc0b76)
@@ -0,0 +1,91 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+
+use Test::More qw(no_plan);
+
+use File::Basename;
+BEGIN {require (dirname($0) . "/mock.pl");};
+
+=head1 DESCRIPTION
+
+Basic tests for tab-completion functionality.
+
+=cut
+
+sub test_tokenize {
+    local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+    my $before_point = shift;
+    my $after_point = shift;
+    
+    my $words = shift;
+    my $word = shift;
+    my $word_point = shift;
+
+    my $ctx = BarnOwl::Completion::Context->new($before_point,
+                                                $after_point);
+
+    is($ctx->line, $before_point . $after_point);
+    is($ctx->point, length $before_point);
+    is_deeply($ctx->words, $words);
+    if (defined($word)) {
+        is($ctx->word, $word, "Correct current word.");
+        is($ctx->word_point, $word_point, "Correct point within word.");
+    }
+}
+
+
+new_ok('BarnOwl::Completion::Context' => ['Hello, W', 'orld']);
+
+no warnings 'qw';
+test_tokenize('Hello, W', 'orld',
+              [qw(Hello, World)], 1, 1);
+
+test_tokenize('Hello, World', '',
+              [qw(Hello, World)], 1, 5);
+
+test_tokenize('', '',
+              [qw()], 0, 0);
+
+test_tokenize('Hello', 'World',
+              [qw(HelloWorld)], 0, 5);
+
+test_tokenize('lorem ipsum dolor ', 'sit amet',
+              [qw(lorem ipsum dolor sit amet)], 3, 0);
+
+test_tokenize(q{error "ls -l failed"}, q{},
+              ['error', 'ls -l failed'], 1, 12);
+
+test_tokenize(q{"a long"' word'}, q{},
+              ['a long word']);
+
+test_tokenize(q{"'"}, q{}, [q{'}]);
+
+test_tokenize(q{"Hello, }, q{World"},
+              [q{Hello, World}], 0, 7);
+
+test_tokenize(q{But 'Hello, }, q{World'},
+              ['But', q{Hello, World}], 1, 7);
+
+test_tokenize(q{But "Hello, }, q{World"''''''""},
+              ['But', q{Hello, World}], 1, 7);
+
+test_tokenize(q{}, q{''Hello},
+              ['Hello'], 0, 0);
+
+test_tokenize(q{"Hello, }, q{World},
+              [q{Hello, World}], 0, 7);
+
+test_tokenize(q{Hello    }, q{World},
+              [qw{Hello World}], 1, 0);
+
+test_tokenize(q{Hello '' ""}, q{ World},
+              ["Hello", '', '', 'World'], 2, 0);
+
+# It's not entirely clear what we should do here. Make a test for the
+# current behavior, so we'll notice if it changes.
+test_tokenize(q{Hello }, q{ World},
+              [qw(Hello World)], 1, -1);
+
+1;
