@@ -1,8 +1,8 @@ #!/usr/locanl/bin/tclsh # FILE: "/home/joze/src/tclreadline/tclreadlineCompleter.tcl" -# LAST MODIFICATION: "Fri Sep 10 03:06:31 1999 (joze)" +# LAST MODIFICATION: "Mon Sep 13 02:21:21 1999 (joze)" # (C) 1998, 1999 by Johannes Zellner, # $Id$ # --- # # tclreadline -- gnu readline for tcl @@ -25,119 +25,52 @@ # johannes@zellner.org # http://www.zellner.org/tclreadline/ # # ================================================================== -# done: -# -# - after -# - append -# - array -# - bgerror -# - binary -# - break -# - catch -# - cd -# - clock -# - close -# - concat -# - continue -# - (ddd is only on M$) -# - encoding -# - eof -# - error -# - eval -# - exec -# - exit -# - expr -# - fblocked -# - fconfigure -# - fcopy -# - file -# - fileevent -# - flush -# - for # TODO -# - foreach # TODO -# - format # TODO -# - gets -# - glob -# - global -# - if # TODO -# - incr -# - index -# - info -# - interp -# - join -# - lappend -# - llength -# - linsert -# - list -# - load -# - lrange -# - lreplace -# - lsearch -# - lsort -# - history -# - load -# - namespace -# - open -# - package -# - pkg_mkIndex -# - proc -# - puts -# - pwd -# - pid -# - read -# - regexp -# - (registry is only on M$) -# - regsub -# - rename -# - (resource is on mac only) -# - return -# - scan # TODO -# - seek -# - socket -# - source -# - split # TODO -# - string -# - subst -# - switch -# - tell -# - time # TODO ?? -# - trace -# - set -# - unknown -# - unset -# - update -# - uplevel -# - upvar -# - variable -# - vwait -# - while # TODO -# + +# TODO: +# +# - tcltest is missing +# +# - last try: as for widgets +# + namespace eval tclreadline { +namespace export \ +TryFromList CompleteFromList DisplayHints Rehash \ +PreviousWord CommandCompletion RemoveUsedOptions \ +HostList ChannelId InChannelId OutChannelId \ +Lindex Llength CompleteBoolean + +#** # TryFromList will return an empty string, if # the text typed so far does not match any of the # elements in list. This might be used to allow # subsequent filename completion by the builtin # completer. # -proc TryFromList {text lst} { +proc TryFromList {text lst {allow ""}} { # puts stderr "(CompleteFromList) \ntext=|$text|" # puts stderr "(CompleteFromList) lst=|$lst|" set pre [GetQuotedPrefix ${text}] - set matches [MatchesFromList $text $lst] + set matches [MatchesFromList $text $lst $allow] # puts stderr "(CompleteFromList) matches=|$matches|" if {1 == [llength $matches]} { ; # unique match # puts stderr \nunique=$matches\n # puts stderr "\n|${pre}${matches}[Right ${pre}]|\n" - return [string trim ${pre}${matches}[Right ${pre}]] + set null [string index $matches 0] + if {"<" == $null || "?" == $null} { + return [string trim "[list $text] $lst"] + } else { + return [string trim ${pre}${matches}[Right ${pre}]] + } } elseif {"" != ${matches}} { # puts stderr \nmore=$matches\n set longest [CompleteLongest ${matches}] # puts stderr longest=|$longest| if {"" == $longest} { @@ -148,39 +81,96 @@ } else { return ""; # nothing to complete } } +#** # CompleteFromList will never return an empty string. # completes, if a completion can be done, or ring # the bell if not. # proc CompleteFromList {text lst} { set result [TryFromList ${text} ${lst}] if {![llength ${result}]} { Alert # return [string trim [list ${text}] ${lst}"] - return [string trim "${text} ${lst}"] + if {[llength ${lst}]} { + return [string trim "${text} ${lst}"] + } else { + return [string trim [list ${text} {}]] + } } else { return ${result} } } -proc MenuFromList {text lst} { - return [CompleteFromList $text $lst] -} -# ??????? -# -# proc MenuFromList {text lst} { -# if {![llength ${text}]} { -# return [string trim "{} ${lst}"] -# } else { -# return [TryFromList ${text} ${lst}] -# } -# } -# - +#** +# CompleteBoolean does a CompleteFromList +# with a list of all valid boolean values. +# +proc CompleteBoolean {text} { + return [CompleteFromList $text {yes no true false 1 0}] +} + +#** +# build a list of all executables which can be +# found in $env(PATH). This is (naturally) a bit +# slow, and should not called frequently. Instead +# it is a good idea to check if the variable +# `executables' exists and then just use it's +# content instead of calling Rehash. +# (see complete(exec)). +# +proc Rehash {} { + + global env + variable executables + + if {![info exists env] || ![array exists env]} { + return + } + if {![info exists env(PATH)]} { + return + } + + set executables 0 + foreach dir [split $env(PATH) :] { + if {[catch [list set files [glob -nocomplain ${dir}/*]]]} { continue } + foreach file $files { + if {[file executable $file]} { + lappend executables [file tail $file] + } + } + } +} + +#** +# build a list hosts from the /etc/hosts file. +# this is only done once. This is sort of a +# dirty hack, /etc/hosts is hardcoded ... +# +proc HostList {} { + # read the host table only once. + # + variable hosts + if {![info exists hosts]} { + catch { + set id [open /etc/hosts r] + set hosts "" + if {0 != ${id}} { + while {-1 != [gets ${id} line]} { + regsub {#.*} ${line} {} line + if {[llength ${line}] >= 2} { + lappend hosts [lindex ${line} 1] + } + } + close $id + } + } + } + return $hosts +} #** # never return an empty string, never complete. # This is useful for showing options lists for example. # @@ -187,22 +177,28 @@ proc DisplayHints {lst} { return [string trim "{} ${lst}"] } #** -# find (partial) matches for `text' in `lst'. -# Ring the bell and return the whole list, if -# the user tries to complete ?..? options or -# <..> hints. +# find (partial) matches for `text' in `lst'. Ring +# the bell and return the whole list, if the user +# tries to complete ?..? options or <..> hints. +# +# MatchesFromList returns a list which is not suitable +# for passing to the readline completer. Thus, +# MatchesFromList should not be called directly but +# from formatting routines as TryFromList. # -proc MatchesFromList {text lst} { +proc MatchesFromList {text lst {allow ""}} { set result "" set text [StripPrefix $text] set null [string index $text 0] - if {"<" == $null || "?" == $null} { - Alert - return $lst + foreach char {< ?} { + if {$char == $null && -1 == [string first $char $allow]} { + Alert + return $lst + } } # puts stderr "(MatchesFromList) text=$text" # puts stderr "(MatchesFromList) lst=$lst" foreach word $lst { if {[string match ${text}* ${word}]} { @@ -226,15 +222,16 @@ proc RemoveUsedOptions {line opts {terminate {}}} { if {[llength ${terminate}]} { if {[regexp -- ${terminate} ${line}]} { return "" + # return ${terminate} } } set new "" foreach word ${opts} { - if {![regexp -- ${word} ${line}]} { + if {-1 == [string first ${word} ${line}]} { lappend new ${word} } } return [string trim ${new}] } @@ -242,11 +239,11 @@ proc Alert {} { puts -nonewline \a flush stdout } - +#** # get the longest common completion # e.g. str == {tcl_version tclreadline_version tclreadline_library} # --> [CompleteLongest ${str}] == "tcl" # proc CompleteLongest {str} { @@ -322,33 +319,54 @@ } else { return [list ${left}${value}${right}] } } -proc InChannelId {text} { - # return [ChannelId ${text} inChannel {stdin}] - return [ChannelId ${text} inChannel] +# the following two channel proc's make use of +# the brandnew (Sep 99) `file channels' command +# but have some fallback behaviour for older +# tcl version. +# +proc InChannelId {text {switches ""}} { + if [catch {set chs [file channels]}] { + set chs {stdin} + } + set result "" + foreach ch $chs { + if {![catch {fileevent $ch readable}]} { + lappend result $ch + } + } + return [ChannelId ${text} $result $switches] } -proc OutChannelId {text} { - # return [ChannelId ${text} outChannel {stdout stderr}] - return [ChannelId ${text} outChannel] +proc OutChannelId {text {switches ""}} { + if [catch {set chs [file channels]}] { + set chs {stdout stderr} + } + set result "" + foreach ch $chs { + if {![catch {fileevent $ch writable}]} { + lappend result $ch + } + } + return [ChannelId ${text} $result $switches] } -proc ChannelId {text {descript channelId} {chs ""}} { +proc ChannelId {text {descript } {chs ""} {switches ""}} { if {"" == ${chs}} { # the `file channels' command is present # only in pretty new versions. # if [catch {set chs [file channels]}] { set chs {stdin stdout stderr} } } - if {[llength [set channel [MatchesFromList ${text} ${chs}]]]} { + if {[llength [set channel [TryFromList ${text} "${chs} ${switches}"]]]} { return ${channel} } else { - return [DisplayHints ${descript}] + return [DisplayHints [string trim "${descript} ${switches}"]] } } proc QuoteQuotes {line} { regsub -all -- \" $line {\"} line @@ -402,11 +420,11 @@ } proc Right {left} { # puts left=$left if {"\"" == $left} { - return {\"} + return "\"" } elseif {"\\\"" == $left} { return "\\\"" } elseif {"\{" == $left} { return "\}" } elseif {"\\\{" == $left} { @@ -432,54 +450,74 @@ incr pos incr found } return $found } + +#** +# make a proper tcl list from an icomplete +# string, that is: remove the junk. This is +# complementary to `IncompleteListRemainder'. +# e.g.: +# for {set i 1} " +# --> for {set i 1} +# +proc ProperList {line} { + set last [expr [string length $line] - 1] + for {set i $last} {$i >= 0} {incr i -1} { + if {![catch {llength [string range $line 0 $i]}]} { + break + } + } + return [string range $line 0 $i] +} + +#** +# return the last part of a line which +# prevents the line from beeing a list. +# This is complementary to `ProperList'. +# +proc IncompleteListRemainder {line} { + set last [expr [string length $line] - 1] + for {set i $last} {$i >= 0} {incr i -1} { + if {![catch {llength [string range $line 0 $i]}]} { + break + } + } + incr i + return [string range $line $i end] +} #** # save `lindex'. works also for non-complete lines # with opening parentheses or quotes. # usage as `lindex'. +# Eventually returns the Rest of an incomplete line, +# if the index is `end' or == [Llength $line]. # proc Lindex {line pos} { if {[catch [list set sub [lindex $line $pos]]]} { - set diff [expr [CountChar $line \{] - [CountChar $line \}]] - # puts stderr diff=$diff - for {set i 0} {$i < $diff} {incr i} { ; # \{ keep the editor happy - append line \} - } - # puts stderr line=$line - if {!$diff || [catch [list set sub [lindex $line $pos]]]} { - if {[expr [CountChar $line \"] % 2]} { append line \" } - } + if {"end" == $pos || [Llength $line] == $pos} { + return [IncompleteListRemainder $line] + } + set line [ProperList $line] + # puts stderr \nproper_line=|$proper_line| if {[catch [list set sub [lindex $line $pos]]]} { return {} } } return $sub } #** # save `llength' (see above). # proc Llength {line} { - set diff 0 if {[catch [list set len [llength $line]]]} { - set diff [expr [CountChar $line \{] - [CountChar $line \}]] - # puts stderr diff=$diff - for {set i 0} {$i < $diff} {incr i} { ; # \{ keep the editor happy - append line \} - } - if {$diff < 0} { - set diff 0 - } - # puts stderr line=$line - if {!$diff || [catch [list set len [llength $line]]]} { - incr diff - if {[expr [CountChar $line \"] % 2]} { append line \" } - } + set line [ProperList $line] if {[catch [list set len [llength $line]]]} { return {} } } - return [expr $len - $diff] + # puts stderr \nline=$line + return $len } proc StripPrefix {text} { # puts "(StripPrefix) text=|$text|" set null [string index $text 0] @@ -488,16 +526,10 @@ } else { return $text } } -proc ListCompletion {text {level -1}} { - # TODO - return "" - # return [VarCompletion ${text} ${level}] -} - proc VarCompletion {text {level -1}} { if {-1 == ${level}} { set level [info level] } else { incr level @@ -535,39 +567,85 @@ unset new } set matches \ [string trim "[uplevel ${level} info vars ${var}*] ${namespaces}"] if {1 == [llength $matches]} { ; # unique match + # check if this unique match is an # array name, (whith no "(" yet). # if {[uplevel ${level} array exists $matches]} { return [VarCompletion ${matches}( ${level}]; # recursion } else { return ${pre}${matches}[Right ${pre}] } } elseif {"" != $matches} { ; # more than one match - #puts stderr "(VarComletion) matches=|$matches|" - #puts stderr "(VarComletion) text=|$text|" -# -# set common [CompleteLongest ${matches}] -# if {"" == ${common}} { -# return [Format ${matches} ${text}] -# } else { -# return [string trim "${pre}${common} ${matches}"] -# } -# return [CompleteFromList ${text} ${matches}] } else { return ""; # nothing to complete } } + +proc CompleteControlStatement {text start end line pos mod pre new_line} { + set pre [GetQuotedPrefix ${pre}] + set cmd [Lindex $new_line 0] + set diff [expr \ + [string length $line] - [string length $new_line]] + if {$diff == [expr $start + 1]} { + set mod1 $mod + } else { + set mod1 $text + set pre "" + } + set new_end [expr $end - $diff] + set new_start [expr $new_end - [string length $mod1]] + # puts "" + # puts new_start=$new_start + # puts new_end=$new_end + # puts new_line=$new_line + # puts mod1=$mod1 + if {$new_start < 0} { + return ""; # when does this occur? + } + # puts stderr "" + # puts stderr start=|$start| + # puts stderr end=|$end| + # puts stderr mod=|$mod| + # puts stderr new_start=|$new_start| + # puts stderr new_end=|$new_end| + # puts stderr new_line=|$new_line| + # puts stderr "" + set res [ScriptCompleter $mod1 $new_start $new_end $new_line] + # puts stderr \n\${pre}\${res}=|${pre}${res}| + if {[string length [Lindex ${res} 0]]} { + return ${pre}${res} + } else { + return ${res} + } + return "" +} + +proc BraceOrControlStatement {text start end line pos mod} { + if {![string length [Lindex $line $pos]]} { + return [list \{ {}]; # \} + } else { + set new_line [string trim [IncompleteListRemainder $line]] + if {![regexp {^([\{\"])(.*)$} $new_line all pre new_line]} { + set pre "" + } + return [CompleteControlStatement $text \ + $start $end $line $pos $mod $pre $new_line] + } +} proc FullQualifiedMatches {qualifier matchlist} { set new "" + if {"" != $qualifier && ![regexp ::$ $qualifier]} { + append qualifier :: + } foreach entry ${matchlist} { - set full ${qualifier}::${entry} + set full ${qualifier}${entry} if {"" != [namespace which ${full}]} { lappend new ${full} } } return ${new} @@ -579,90 +657,93 @@ proc CommandsOnlyCompletion {cmd} { return [CommandCompletion ${cmd} commands] } -proc CommandCompletion {cmd {action both} {spc ::}} { +proc CommandCompletion {cmd {action both} {spc ::} {pre UNDEFINED}} { # puts stderr "(CommandCompletion) cmd=|$cmd|" # puts stderr "(CommandCompletion) action=|$action|" # puts stderr "(CommandCompletion) spc=|$spc|" + + # get the leading colons in `cmd'. + if {"UNDEFINED" == $pre} { + regexp {^:*} ${cmd} pre + } + # puts stderr \npre=|$pre| + + set cmd [StripPrefix ${cmd}] set quali [namespace qualifiers ${cmd}] - if {[llength ${quali}]} { - set rec [CommandCompletion [namespace tail ${cmd}] ${action} ${quali}] - return [FullQualifiedMatches ${quali} ${rec}] + if {[string length ${quali}]} { + # puts stderr \nquali=|$quali| + set matches [CommandCompletion \ + [namespace tail ${cmd}] ${action} ${spc}${quali} ${pre}] + # puts stderr \nmatches1=|$matches| + return $matches } - # puts stderr \ncmd=|$cmd|\n set cmd [string trim ${cmd}]* + # puts stderr \ncmd=|$cmd|\n if {"procs" != ${action}} { - set commands [namespace eval $spc "info commands [QuoteQuotes ${cmd}]"] - # puts stderr commands=|$commands| + set all_commands [namespace eval $spc [list info commands ${cmd}]] + # puts stderr all_commands=|$all_commands| + set commands "" + foreach command $all_commands { + if {[namespace eval $spc [list namespace origin $command]] == \ + [namespace eval $spc [list namespace which $command]]} { + lappend commands $command + } + } } else { set commands "" } if {"commands" != ${action}} { - set procs [namespace eval $spc "info procs [QuoteQuotes ${cmd}]"] + set all_procs [namespace eval $spc [list info procs ${cmd}]] # puts stderr procs=|$procs| + set procs "" + foreach proc $all_procs { + if {[namespace eval $spc [list namespace origin $command]] == \ + [namespace eval $spc [list namespace which $command]]} { + lappend procs $command + } + } } else { set procs "" } set matches [namespace eval $spc concat ${commands} ${procs}] -# -# foreach match ${matches} { -# set full ${spc}::${match} -# if {"" != [namespace which ${full}]} { -# lappend new bla::${full} -# } -# } -# set namespaces [namespace children $spc ${cmd}] + if {![llength ${matches}] && 1 == [llength ${namespaces}]} { - set namespaces [string trim ${namespaces}] - regsub {^([^:])} $namespaces {::\1} namespaces - # set matches [namespace eval ${namespaces} \ - # {concat [info commands] [info procs]}] - if {"procs" != ${action}} { - set n_commands [namespace eval ${namespaces} "info commands"] - } else { - set n_commands "" - } - if {"commands" != ${action}} { - set n_procs [namespace eval ${namespaces} "info procs"] - } else { - set n_procs "" - } - set matches [string trim "${n_commands} ${n_procs}"] - if {[llength ${matches}]} { -# -# foreach match ${matches} { -# set full ${namespaces}::${match} -# if {"" != [namespace which ${full}]} { -# lappend new ${namespaces}::${match} -# } -# } -# -# set matches ${new} -# unset new -# - set matches [FullQualifiedMatches ${namespaces} ${matches}] - set namespaces "" - } - return [string trim "${matches} ${namespaces}"] - } else { - return [string trim "${matches} ${namespaces}"] - } + set matches [CommandCompletion {} ${action} ${namespaces} ${pre}] + # puts stderr \nmatches=|$matches| + return $matches + } + + # make `namespaces' having exactly + # the same number of colons as `cmd'. + # + regsub -all {^:*} $spc $pre spc + + set matches [FullQualifiedMatches ${spc} ${matches}] + # puts stderr \nmatches3=|$matches| + return [string trim "${matches} ${namespaces}"] } #** # check, if the first argument starts with a '[' # and must be evaluated before continuing. -# NOTE: eventually modifies all arguments. +# NOTE: trims the `line'. +# eventually modifies all arguments. # DATE: Sep-06-1999 # proc EventuallyEvaluateFirst {partT startT endT lineT} { # return; # disabled upvar $partT part $startT start $endT end $lineT line + + set oldlen [string length ${line}] set line [string trim ${line}] + set diff [expr [string length $line] - $oldlen] + incr start $diff + incr end $diff set char [string index ${line} 0] if {{[} != ${char} && {$} != ${char}} {return} set pos 0 @@ -693,13 +774,25 @@ # start == 5 # end == 7 # line == "$puts $b" # proc ScriptCompleter {part start end line} { + # puts stderr "(ScriptCompleter) |$part| $start $end |$line|" - variable known_cmds + + # if the character before the cursor is a terminating + # quote and the user wants completion, we insert a white + # space here. + # + set char [string index $line [expr $end - 1]] + if {"\}" == $char} { + append $part " " + return [list $part] + } + if {{$} == [string index $part 0]} { + # check for a !$ history event # if {$start > 0} { if {{!} == [string index $line [expr $start - 1]]} { return "" @@ -708,19 +801,11 @@ # variable completion. Check first, if the # variable starts with a plain `$' or should # be enclosed in braces. # set var [string range $part 1 end] -# -# if {"\{" == [string index $part 1]} { -# set var [string range $part 2 end] -# set left "\{" -# } else { -# set left "" -# set var [string range $part 1 end] -# } -# + # check if $var is an array name, which # already has already a "(" somewhere inside. # if {"" != [set vc [VarCompletion $var]]} { if {"" == [lindex $vc 0]} { @@ -730,10 +815,11 @@ } # puts stderr vc=|$vc| } else { return "" } + # SCENARIO: # # % puts bla; put $b # part == put # start == 10 @@ -751,34 +837,58 @@ set new_line [lindex $sub 1] # puts stderr "(SplitLine) $new_start $new_end $new_line" return [ScriptCompleter $part $new_start $new_end $new_line] } elseif {0 == [set pos [PartPosition part start end line]]} { # puts stderr "(PartPosition) $part $start $end $line" - # set matches [array names known_cmds "[string trim ${part}]*"] set all [CommandCompletion ${part}] # puts stderr "(ScriptCompleter) all=$all" #puts \nmatches=$matches\n # return [Format $all $part] return [TryFromList $part $all] } else { + # try to use $pos further ... # puts stderr |$line| + # if {"." == [string index [string trim ${line}] 0]} { set alias WIDGET } else { - set alias [lindex [QuoteQuotes ${line}] 0] + + # the double `lindex' strips {} or quotes. + # the subst enables variables containing + # command names. + # + set alias [uplevel [info level] \ + subst [lindex [lindex [QuoteQuotes ${line}] 0] 0]] + + # make `alias' a fully qualified name. + # this can raise an error, if alias is + # no valid command. + # + if {[catch [list set alias [namespace origin $alias]]]} { + return "" + } + + # strip leading ::'s. + # + regsub -all {^::} $alias {} alias + set namespc [namespace qualifiers $alias] + set alias [namespace tail $alias] } + foreach cmd [list ${alias} tclreadline_complete_unknown] { - if {"" != [namespace eval ::tclreadline \ + # puts stderr ${namespc}complete(${cmd}) + if {"" != [namespace eval ::tclreadline::${namespc} \ [list info procs complete(${cmd})]] } { + # puts found=|complete($cmd)| # to be more error-proof, we check here, # if complete($cmd) takes exactly 5 arguments. # if {6 != [set arguments [llength \ - [namespace eval ::tclreadline \ - [list info args complete(${cmd})]]]] + [namespace eval ::tclreadline::${namespc} \ + [list info args complete($cmd)]]]] } { error [list complete(${cmd}) takes ${arguments} \ arguments, but should take exactly 6.] } @@ -786,30 +896,23 @@ # set mod [StripPrefix $part] # puts stderr mod=$mod if {[catch [list set script_result \ - [complete(${cmd}) $part \ - $start $end $line $pos $mod]] ::tclreadline::errorMsg] + [namespace eval ::tclreadline::${namespc} \ + [list complete(${cmd}) $part $start $end $line $pos $mod]]]\ + ::tclreadline::errorMsg] } { - error "error during evaluation of `complete(${cmd})'" + error [list error during evaluation of `complete(${cmd})'] } # puts stderr \nscript_result=|${script_result}| return ${script_result} } + # set namespc ""; # no qualifiers for tclreadline_complete_unknown } # no specific command completer found. - if {"" != [array names known_cmds $cmd]} { - set current [lindex $known_cmds($cmd) $pos] - if {"" != $current && "" == [string trim $part]} { - return $current - } else { - return "" - } - } else { - return "" - } + return "" } error "{NOTREACHED (this is probably an error)}" } @@ -828,22 +931,17 @@ return [CompleteFromList ${text} { cancel idle info}] } 2 { switch -- $sub { cancel { - set after_info [after info] - if {![llength $after_info]} { - return [DisplayHints