# $Id: Completion.pm,v 1.20 2003/09/10 13:36:26 pajas Exp $
package XML::XSH::Completion;
use XML::XSH::CompletionList;
use XML::XSH::Functions qw();
use strict;
our @PATH_HASH;
our $M=qr/(?:^|[;}]|\s+{)\s*/;
our $match_sv=qr/\$([a-zA-Z0-9_]*)$/; # scalar variable completion
our $match_nv=qr/\%([a-zA-Z0-9_]*)$/; # node-list variable completion
our $match_command=qr/${M}[^=\s]*$/; # command completion
our $match_func=qr/${M}(?:call|undef|undefine)\s+(\S*)$/; # function name completion
our $match_nodetype=qr/${M}x?(?:insert|add)\s+(\S*)$/; # node-type completion
our $match_doc=qr/${M}(?:close|doc[-_]info|dtd|enc)\s+(\S*)$|${M}clone\s+[a-zA-Z0-9_]*\s*=\s*[a-zA-Z0-9_]*$|${M}create\s+[a-zA-Z0-9_]*/; # docid completion
our $match_clone_doc=qr/${M}clone\s+[a-zA-Z0-9_]*$/;
our $match_help=qr/${M}(?:\?|help)\s+(\S*)$/; # help topic completion
our $match_open_flag1=qr/${M}open(?:\s+|([-_]))([A-Z]*)$/;
our $match_open_flag2=qr/${M}(?:open\s+|(open[-_]))(html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+[A-Z]*|([-_])[A-Za-z]*)$/;
our $match_open_doc=qr/${M}(open)(?:\s+|_|-)(?:(?:html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+|_|-))?(?:(?:file|pipe|string|FILE|PIPE|STRING)\s+)?([a-zA-Z0-9_]*)$/;
our $match_open_filename=qr/${M}(?:open(?:\s+|_|-)(?:(?:html|xml|docbook|HTML|XML|DOCBOOK)(?:\s+|_|-))?(?:(?:file|FILE)\s+)?)?[a-zA-Z0-9_]+\s*=\s*(\S*)$/;
our $match_save_flag1=qr/${M}save(?:\s+|([-_]))([A-Z]*)$/;
our $match_save_flag2=qr/${M}(?:save\s+|(save[-_]))(html|xml|xinclude|HTML|XML|XINCLUDE|XInclude)(?:\s+[A-Z]*|([-_])[A-Za-z]*)$/;
our $match_save_doc=qr/${M}(save)(?:\s+|_|-)(?:(?:html|xml|xinclude|HTML|XML|XInclude|XINCLUDE)(?:\s+|_|-))?(?:(?:file|pipe|string|FILE|PIPE|STRING)\s+)?([a-zA-Z0-9_]*)$/;
our $match_save_filename=qr/${M}save(?:\s+|_|-)(?:(?:html|xml|xinclude|HTML|XML|XInclude|XINCLUDE)(?:\s+|_|-))?(?:(?:file|FILE)\s+)?[a-zA-Z0-9_]+\s+(\S*)$/;
our $match_filename=qr/${M}(?:\.|include)\s+(\S*)$/;
our $match_dir=qr/${M}(?:lcd)\s+(\S*)$/;
our $match_path_filename=qr/${M}(?:system\s|exec\s|\!)\s*\S*$|\s\|\s*\S*$/;
our $match_no_xpath=join '|',@XML::XSH::CompletionList::XSH_NOXPATH_COMMANDS;
our $match_no=qr/${M}(?:${match_no_xpath}|create\s+[a-zA-Z0-9_]*\s)\s*$/;
# PATH-completion: system, !, exec, |,
our @nodetypes = qw(element attribute attributes text cdata pi comment chunk entity_reference);
our @openflags1 = qw(HTML XML DOCBOOK);
our @openflags2 = qw(FILE PIPE STRING);
our @saveflags1 = qw(HTML XML XINCLUDE);
our @saveflags2 = qw(FILE PIPE STRING);
sub perl_complete {
my($word,$line,$pos) = @_;
my $endpos=$pos+length($word);
cpl('perl',$word,$line,$pos,$endpos);
}
sub gnu_complete {
my($text, $line, $start, $endpos) = @_;
&main::_term()->Attribs->{completion_append_character} = ' ';
my @result=cpl('gnu',$text,$line,$start,$endpos);
# find longest common match. Can anybody show me how to persuade
# T::R::Gnu to do this automatically? Seems expensive.
return () unless @result;
my($newtext) = $text;
for (my $i = length($text)+1;;$i++) {
last unless length($result[0]) && length($result[0]) >= $i;
my $try = substr($result[0],0,$i);
my @tries = grep {substr($_,0,$i) eq $try} @result;
# warn "try[$try]tries[@tries]";
if (@tries == @result) {
$newtext = $try;
} else {
last;
}
}
($newtext,@result);
}
sub complete_set_term_char {
my ($type,$char)=@_;
if ($type eq 'perl') {
$readline::rl_completer_terminator_character = $char;
} else {
&main::_term()->Attribs->{completion_append_character} = $char;
}
}
sub complete_filename {
my ($type,$word)=@_;
if ($type eq 'perl') {
return eval { map { s:\@$::; $_ } readline::rl_filename_list($word); };
} else {
return eval { map { s:\@$::; $_ } Term::ReadLine::Gnu::XS::rl_filename_list($word) };
}
}
sub rehash_path_hash {
my %result;
my $dh;
my $pdelim= $^O eq 'MSWin32' ? '\\' : '/';
my $delim=($^O eq 'MSWin32' ? ';' : ':');
my @path=grep /\S/,split($delim,$ENV{PATH});
foreach my $dir (@path) {
local *DIR;
if (opendir DIR, $dir) {
my @files=grep { -f "$dir$pdelim$_" and -x "$dir$pdelim$_" } readdir(DIR);
@result{@files}=();
closedir DIR;
}
}
@PATH_HASH=sort keys %result;
}
sub complete_system_command {
my ($type,$word)=@_;
my $pdelim= $^O eq 'MSWin32' ? '\\' : '/';
if (index($word,$pdelim)>=0) {
return grep -x,complete_filename($type,$word);
}
unless (defined @PATH_HASH) {
rehash_path_hash();
}
return grep {index($_,$word)==0} @PATH_HASH;
}
sub cpl {
my($type,$word,$line,$pos,$endpos) = @_;
if (substr($line,0,$endpos)=~$match_sv) {
return map {'$'.$_} grep { index($_,$1)==0 } XML::XSH::Functions::string_vars;
} elsif (substr($line,0,$endpos)=~$match_nv) {
return map {'%'.$_} grep { index($_,$1)==0 } XML::XSH::Functions::nodelist_vars;
} elsif (substr($line,0,$endpos)=~$match_func) {
return grep { index($_,$1)==0 } XML::XSH::Functions::defs;
} elsif (substr($line,0,$endpos)=~$match_nodetype) {
return grep { index($_,$1)==0 } @nodetypes;
} elsif (substr($line,0,$endpos)=~$match_help) {
return grep { index($_,$1)==0 } keys %XML::XSH::Help::HELP;
} elsif (substr($line,0,$endpos)=~$match_open_flag1) {
my $prefix;
$prefix='open'.$1 if ($1 ne "");
return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @openflags1, @openflags2;
} elsif (substr($line,0,$endpos)=~$match_open_flag2) {
my $prefix;
if ($3 ne "") {
$prefix=$1.uc($2).$3;
return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @openflags2;
} else {
return grep { index($_,uc($word))==0 } @openflags2;
}
} elsif (substr($line,0,$endpos)=~$match_save_flag1) {
if ($1) {
my $prefix;
$prefix='save'.$1;
return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @saveflags1, @saveflags2;
} else {
return grep { index($_,uc($word))==0 } @saveflags1, @saveflags2, XML::XSH::Functions::docs();
}
} elsif (substr($line,0,$endpos)=~$match_save_flag2) {
my $prefix;
if ($3 ne "") {
$prefix=$1.uc($2).$3;
return grep { index(uc($_),uc($word))==0 } map {$prefix.$_} @saveflags2;
} else {
return grep { index($_,uc($word))==0 } @saveflags2, XML::XSH::Functions::docs();
}
} elsif (substr($line,0,$pos)=~$match_command) {
return grep { index($_,$word)==0 } @XML::XSH::CompletionList::XSH_COMMANDS;
} elsif (substr($line,0,$endpos)=~$match_doc ||
substr($line,0,$endpos)=~$match_save_doc) {
return grep { index($_,$word)==0 } XML::XSH::Functions::docs();
} elsif (substr($line,0,$endpos)=~$match_clone_doc ||
substr($line,0,$endpos)=~$match_open_doc) {
complete_set_term_char($type,'=');
return grep { index($_,$word)==0 } XML::XSH::Functions::docs();
} elsif (substr($line,0,$endpos)=~$match_open_filename ||
substr($line,0,$endpos)=~$match_save_filename ||
substr($line,0,$endpos)=~$match_filename) {
my @result=complete_filename($type,$word);
if (@result==1 and -d $result[0]) {
complete_set_term_char($type,'');
} else {
complete_set_term_char($type,' ');
}
return @result;
} elsif (substr($line,0,$endpos)=~$match_dir) {
my @result=grep -d, complete_filename($type,$word);
if (@result==1) {
complete_set_term_char($type,' ');
} else {
complete_set_term_char($type,'');
}
return @result;
} elsif (substr($line,0,$endpos)=~$match_path_filename) {
my @result=complete_system_command($type,$word);
if (@result==1 and -d $result[0]) {
complete_set_term_char($type,'');
} else {
complete_set_term_char($type,' ');
}
return @result;
} elsif (substr($line,0,$endpos)=~$match_no) {
return ();
} else {
complete_set_term_char($type,'');
return xpath_complete($line,$word,$pos);
}
}
sub xpath_complete_str {
my $str = reverse($_[0]);
my $debug = $_[1];
my $result="";
my $NAMECHAR = '[-_.[:alnum:]]';
my $NNAMECHAR = '[-:_.[:alnum:]]';
my $NAME = "${NAMECHAR}*${NNAMECHAR}*[_.[:alpha:]]";
my $WILDCARD = '\*(?!\*|${NAME}|\)|\]|\.)';
my $OPER = qr/(?:[,=<>\+\|]|-(?!${NAME})|(?:vid|dom|dna|ro)(?=\s*\]|\s*\)|\s*[0-9]+(?!${NNAMECHAR})|\s+{$NAMECHAR}|\s+\*))/;
print "'$str'\n" if $debug;
my $localmatch;
STEP0:
if ($str =~ /\G\s*[\]\)]/gsco) {
print "No completions after ] or )\n" if $debug;
return;
}
STEP1:
if ( $str =~ /\G(${NAMECHAR}+)?(?::(${NAMECHAR}+))?/gsco ) {
if ($2 ne "") {
$localmatch=reverse($2).":".reverse($1);
if ($1 ne "") {
$result=reverse($2).':*[starts-with(local-name(),"'.reverse($1).'")]'.$result;
} else {
$result=reverse($2).':*'.$result;
}
} else {
$localmatch=reverse($1);
$result='*[starts-with(name(),"'.$localmatch.'")]'.$result;
}
} else {
$result='*'.$result;
}
if ($str =~ /\G\@/gsco) {
$result="@".$result;
}
STEP2:
print "STEP2-LOCALMATCH: $localmatch\n" if $debug;
print "STEP2: $result\n" if $debug;
print "STEP2-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
while ($str =~ m/\G(::|:|\@|${NAME}\$?|\/\/|\/|${WILDCARD}|\)|\])/gsco) {
print "STEP2-MATCH: '$1'\n" if $debug;
if ($1 eq ')' or $1 eq ']') {
# eat ballanced upto $1
my @ballance=(($1 eq ')' ? '(' : '['));
$result=$1.$result;
print "STEP2: Ballanced $1\n" if $debug;
do {
$result=reverse($1).$result if $str =~ m/\G([^]["'()]+)/gsco; # skip normal characters
return ($result,$localmatch) unless $str =~ m/\G(.)/gsco;
if ($1 eq $ballance[$#ballance]) {
pop @ballance;
} elsif ($1 eq ')') {
push @ballance, '(';
} elsif ($1 eq ']') {
push @ballance, '[';
} elsif ($1 eq '"') {
push @ballance, '"';
} elsif ($1 eq "'") {
push @ballance, "'";
} else {
print STDERR "Error 2: lost in an expression on '$1' ";
print STDERR reverse(substr($str,pos()))."\n";
print "-> $result\n";
return undef;
}
$result=$1.$result;
} while (@ballance);
} else {
$result=reverse($1).$result;
}
}
STEP3:
print "STEP3: $result\n" if $debug;
print "STEP3-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
if (substr($result,0,1) eq '/') {
if ($str =~ /\G['"]/gsco) {
print STDERR "Error 1: unballanced '$1'\n";
return undef;
} elsif ($str =~ /\G(?:\s+['"]|\(|\[|${OPER})/gsco) {
return ($result,$localmatch);
}
return ($result,$localmatch); # uncertain!!!
} else {
return ($result,$localmatch) if ($str=~/\G\s+(?=${OPER})/gsco);
}
STEP4:
print "STEP4: $result\n" if $debug;
print "STEP4-STR: ".reverse(substr($str,pos($str)))."\n" if $debug;
my @ballance;
do {
$str =~ m/\G([^]["'()]+)/gsco; # skip normal characters
print "STEP4-MATCH '".reverse($1)."'\n" if $debug;
return ($result,$localmatch) unless $str =~ m/\G(.)/gsco;
print "STEP4-BALLANCED '$1'\n" if $debug;
if (@ballance and $1 eq $ballance[$#ballance]) {
pop @ballance;
} elsif ($1 eq ')') {
push @ballance, '(';
} elsif ($1 eq ']') {
push @ballance, '[';
} elsif ($1 eq '"') {
push @ballance, '"';
} elsif ($1 eq "'") {
push @ballance, "'";
} elsif (not(@ballance) and $1 eq '[') {
print "STEP4-PRED2STEP '$1'\n" if $debug;
$result='/'.$result;
goto STEP2;
}
} while (@ballance);
goto STEP4;
}
sub xpath_complete {
my ($line, $word,$pos)=@_;
return () unless $XML::XSH::Functions::XPATH_COMPLETION;
my $str=XML::XSH::Functions::toUTF8($XML::XSH::Functions::QUERY_ENCODING,
substr($line,0,$pos).$word);
my ($xp,$local) = xpath_complete_str($str,0);
# XML::XSH::Functions::__debug("COMPLETING $_[0] local $local as $xp\n");
return () if $xp eq "";
my ($docid,$q) = ($xp=~/^(?:([a-zA-Z_][a-zA-Z0-9_]*):(?!:))?((?:.|\n)*)$/);
if ($docid ne "" and not XML::XSH::Functions::_doc($docid)) {
$q=$docid.":".$q;
$docid="";
}
my ($id,$query,$doc)=XML::XSH::Functions::_xpath([$docid,$q]);
return () unless (ref($doc));
my $ql= eval { XML::XSH::Functions::find_nodes([$id,$query]) };
return () if $@;
my %names;
@names{ map {
XML::XSH::Functions::fromUTF8($XML::XSH::Functions::QUERY_ENCODING,
substr(substr($str,0,
length($str)
-length($local)).
$_->nodeName(),$pos))
} @$ql}=();
my @completions = sort { $a cmp $b } keys %names;
# print "completions so far: @completions\n";
if (($XML::XSH::Functions::XPATH_AXIS_COMPLETION eq 'always' or
$XML::XSH::Functions::XPATH_AXIS_COMPLETION eq 'when-empty' and !@completions)
and $str =~ /[ \n\t\r|([=<>+-\/]([[:alpha:]][-:[:alnum:]]*)?$/ and $1 !~ /::/) {
# complete XML axis
my ($pre,$axpart)=($word =~ /^(.*[^[:alnum:]])?([[:alpha:]][-[:alnum:]:]*)/);
# print "\nWORD: $word\nPRE: $pre\nPART: $axpart\nSTR:$str\n";
foreach my $axis (qw(following preceding following-sibling
preceding-sibling
parent ancestor ancestor-or-self descendant self
descendant-or-self child attribute namespace)) {
if ($axis =~ /^${axpart}/) {
push @completions, "${pre}${axis}::";
}
}
}
return @completions;
}
1;