Utils/Termlib: Implement LineEditor auxiliary display support
g0dil [Thu, 8 Jan 2009 20:14:30 +0000 (20:14 +0000)]
Utils/Termlib: Implement generic completion support
Utils/Console: Provide access to path parsing in CommandParser
Utils/Console: Implement command name completion in interactive console

git-svn-id: https://svn.berlios.de/svnroot/repos/senf/trunk@1048 270642c3-0616-0410-b53a-bc976706d245

Utils/Console/LineEditor.cc
Utils/Console/LineEditor.hh
Utils/Console/Parse.cc
Utils/Console/Parse.hh
Utils/Console/Parse.ih
Utils/Console/Server.cci
Utils/Console/Server.hh
Utils/Termlib/Editor.cc
Utils/Termlib/Editor.hh
config.hh

index ab2f548..95f1153 100644 (file)
@@ -28,6 +28,7 @@
 
 // Custom includes
 #include "../Logger/SenfLog.hh"
+#include "../../Utils/range.hh"
 
 //#include "LineEditor.mpp"
 #define prefix_
@@ -76,6 +77,10 @@ LineEditorClientReader(Client & client, LineEditorSwitcher & switcher)
     editor_.prompt(promptString());
     editor_.defineKey(senf::term::KeyParser::Ctrl('D'),
                       senf::membind(&LineEditorClientReader::deleteCharOrExit, this));
+    editor_.defineKey(senf::term::KeyParser::Tab,
+                      boost::bind(&term::bindings::complete,
+                                  _1, 
+                                  senf::membind(&LineEditorClientReader::completePath, this)));
 }
 
 prefix_ void senf::console::detail::LineEditorClientReader::v_setupFailed()
@@ -122,6 +127,77 @@ senf::console::detail::LineEditorClientReader::deleteCharOrExit(term::LineEditor
         term::bindings::deleteChar(editor);
 }
 
+prefix_ void senf::console::detail::LineEditorClientReader::
+completePath(term::LineEditor & editor, unsigned b, unsigned e,
+             std::vector<std::string> & completions)
+{
+    std::string base (editor.text().substr(b,e));
+    CommandParser parser;
+    ParseCommandInfo cmd;
+    try {
+        parser.parsePath(base, cmd);
+    }
+    catch (CommandParser::ParserErrorException & ex) {
+        return;
+    }
+    ParseCommandInfo::TokensRange path (cmd.commandPath());
+    if (path.empty()) {
+        DirectoryNode::ChildrenRange cs (client().cwd().children());
+        for (DirectoryNode::ChildrenRange::iterator i (cs.begin()); i != cs.end(); ++i) {
+            completions.push_back(i->first);
+            if (i->second->isDirectory())
+                completions.push_back(i->first + "/");
+        }
+        return;
+    }
+    
+    ParseCommandInfo::TokensRange::const_iterator i (path.begin());
+    ParseCommandInfo::TokensRange::const_iterator const i_end (boost::prior(path.end()));
+    DirectoryNode * dir (& client().cwd());
+    std::string basePath;
+    for (; i != i_end; ++i)
+        if (*i == NoneToken()) {
+            if (i == path.begin()) {
+                dir = & client().root();
+                basePath = "/";
+            }
+        }
+        else if (*i == WordToken("..")) {
+            DirectoryNode * parent (dir->parent().get());
+            if (parent) dir = parent;
+            basePath += "../";
+        }
+        else if (*i == WordToken(".")) 
+            ;
+        else {
+            if (dir->hasChild(i->value())) {
+                dir = & dir->getDirectory(i->value());
+                basePath += i->value() + "/";
+            } else {
+                DirectoryNode::ChildrenRange cs (dir->completions(i->value()));
+                if (has_one_elt(cs)) {
+                    GenericNode * node (cs.begin()->second.get());
+                    if (node->isLink())
+                        node = & static_cast<LinkNode*>(node)->follow();
+                    if (!node->isDirectory())
+                        return;
+                    dir = static_cast<DirectoryNode*>(node);
+                    basePath += cs.begin()->first + "/";
+                }
+                else
+                    return;
+            }
+        }
+
+    DirectoryNode::ChildrenRange cs (dir->completions(i->value()));
+    for (DirectoryNode::ChildrenRange::iterator j (cs.begin()); j != cs.end(); ++j) {
+        completions.push_back(basePath + j->first);
+        if (j->second->isDirectory())
+            completions.push_back(basePath + j->first + "/");
+    }
+}
+
 ///////////////////////////////cc.e////////////////////////////////////////
 #undef prefix_
 //#include "LineEditor.mpp"
index 840ce5a..39e13b6 100644 (file)
@@ -74,6 +74,8 @@ namespace detail {
         // Editor callbacks
         void executeLine(std::string const & text);
         void deleteCharOrExit(term::LineEditor & editor);
+        void completePath(term::LineEditor & editor, unsigned b, unsigned e, 
+                          std::vector<std::string> & completions);
 
         term::LineEditor editor_;
         LineEditorSwitcher * switcher_;
index dc1dbce..e4e1e66 100644 (file)
@@ -349,6 +349,29 @@ prefix_ void senf::console::CommandParser::parseArguments(std::string const & ar
     }
 }
 
+prefix_ void senf::console::CommandParser::parsePath(std::string const & path,
+                                                     ParseCommandInfo & info)
+{
+    typedef boost::spirit::position_iterator<std::string::const_iterator> PositionIterator;
+    PositionIterator b (path.begin(), path.end(), std::string("<unknown>"));
+    PositionIterator e (path.end(), path.end(), std::string("<unknown>"));
+    detail::ParseDispatcher::BindInfo bind (impl().dispatcher, info);
+    boost::spirit::parse_info<PositionIterator> result;
+    try {
+        result = boost::spirit::parse( b, e, 
+                                       impl().grammar.use_parser<Impl::Grammar::PathParser>(),
+                                       impl().grammar.use_parser<Impl::Grammar::SkipParser>() );
+    }
+    catch (boost::spirit::parser_error<Impl::Grammar::Errors, PositionIterator> & ex) {
+        throwParserError(ex);
+    }
+    if (! result.full) {
+        boost::spirit::file_position pos (result.stop.get_position());
+        throw ParserErrorException("path expected")
+            << "\nat " << pos.file << ":" << pos.line << ":" << pos.column;
+    }
+}
+
 struct senf::console::CommandParser::SetIncremental
 {
     SetIncremental(CommandParser & parser) : parser_ (parser) {
index cfa871b..5e8b0ac 100644 (file)
@@ -599,6 +599,11 @@ namespace console {
                                              of the command). The argument tokens are written into
                                              \a info. */
 
+        void parsePath(std::string const & path, ParseCommandInfo & info);
+                                        ///< Parse \a path
+                                        /**< parsePath() parses the string \a path as an arbitrary
+                                             command path. The result is written into \a info. */
+
         std::string::size_type parseIncremental(std::string const & commands, Callback cb);
                                         ///< Incremental parse
                                         /**< An incremental parse will parse all complete statements
index afb4b78..8d3d35d 100644 (file)
@@ -28,6 +28,7 @@
 
 // Custom includes
 #include <vector>
+#include "../../config.hh"
 #include <boost/spirit.hpp>
 #include <boost/spirit/utility/grammar_def.hpp>
 #include <boost/spirit/dynamic.hpp>
@@ -51,7 +52,7 @@ namespace detail {
         ///////////////////////////////////////////////////////////////////////////
         // Start rules
 
-        enum { CommandParser, SkipParser, ArgumentsParser };
+        enum { CommandParser, SkipParser, ArgumentsParser, PathParser };
 
         ///////////////////////////////////////////////////////////////////////////
         // The parse context (variables needed while parsing)
@@ -97,12 +98,13 @@ namespace detail {
         struct definition 
             : public boost::spirit::grammar_def< boost::spirit::rule<Scanner>, 
                                                  boost::spirit::rule<Scanner>,
+                                                 boost::spirit::rule<Scanner>,
                                                  boost::spirit::rule<Scanner> >
         {
             boost::spirit::rule<Scanner> command, path, argument, word, string, hexstring, token,
                 punctuation, hexbyte, balanced_tokens, simple_argument, complex_argument, builtin, 
                 skip, statement, relpath, abspath, arguments, group_start, group_close, 
-                statement_end;
+                statement_end, opt_path;
             boost::spirit::chset<> special_p, punctuation_p, space_p, invalid_p, word_p;
             boost::spirit::distinct_parser<> keyword_p;
 
@@ -260,6 +262,11 @@ namespace detail {
                                                   [ token_ = construct_<Token>(Token::HexString,
                                                                                str_) ]
                     ;
+                
+                opt_path
+                    = ! path                      [ bind(&PD::beginCommand)(d_, path_) ]
+                                                  [ bind(&PD::endCommand)(d_) ]
+                    ;
 
                 path                    // Returns value in context.path
                     =    eps_p                    [ clear(path_) ]
@@ -349,7 +356,8 @@ namespace detail {
                 start_parsers(
                     command,            // CommandParser
                     skip,               // SkipParser
-                    arguments           // ArgumentsParser
+                    arguments,          // ArgumentsParser
+                    opt_path            // PathParser
                 );
 
                 BOOST_SPIRIT_DEBUG_TRACE_RULE(command,1);
index 9f3f4e6..fb6e82d 100644 (file)
@@ -139,6 +139,12 @@ prefix_ senf::console::DirectoryNode & senf::console::Client::root()
     return server_.root();
 }
 
+prefix_ senf::console::DirectoryNode & senf::console::Client::cwd()
+    const
+{
+    return executor_.cwd();
+}
+
 prefix_ senf::console::Server::Mode senf::console::Client::mode()
     const
 {
index 4fac812..431221b 100644 (file)
@@ -173,6 +173,7 @@ namespace console {
         std::ostream & stream();
         std::string promptString() const;
         DirectoryNode & root() const;
+        DirectoryNode & cwd() const;
         Server::Mode mode() const;
         void write(std::string const & data) const;
 
index d8bd15e..f268efb 100644 (file)
@@ -39,14 +39,15 @@ prefix_ senf::term::BaseEditor::BaseEditor(AbstractTerminal & terminal)
       keyTimeout_ (senf::ClockService::milliseconds(DEFAULT_KEY_TIMEOUT_MS)),
       timer_ ("senf::term::BaseEditor::keySequenceTimeout", 
               senf::membind(&BaseEditor::keySequenceTimeout, this)),
-      column_ (0u)
+      column_ (0u), displayHeight_ (1u), line_ (0u)
 {
     terminal_->setCallbacks(*this);
 }
 
 prefix_ void senf::term::BaseEditor::newline()
 {
-    write("\r\n");
+    reset();
+    write("\n");
     write(tifo_.getString(Terminfo::properties::ClrEol));
     column_ = 0;
 }
@@ -130,12 +131,71 @@ prefix_ void senf::term::BaseEditor::maybeClrScr()
         write(tifo_.getString(Terminfo::properties::ClearScreen));
 }
 
+prefix_ void senf::term::BaseEditor::toLine(unsigned l)
+{
+    if (l >= height())
+        l = height() - 1;
+    unsigned ll (l);
+    if (ll >= displayHeight_)
+        ll = displayHeight_-1;
+    if (ll > line_) {
+        if (tifo_.hasProperty(Terminfo::properties::ParmDownCursor)) {
+            write(tifo_.formatString(Terminfo::properties::ParmDownCursor, ll - line_));
+            line_ = ll;
+        }
+        else {
+            char const * cud1 (tifo_.getString(Terminfo::properties::CursorDown));
+            while (ll > line_) {
+                write(cud1);
+                ++line_;
+            }
+        }
+    }
+    else if (ll < line_) {
+        if (tifo_.hasProperty(Terminfo::properties::ParmUpCursor)) {
+            write(tifo_.formatString(Terminfo::properties::ParmUpCursor, line_ - ll));
+            line_ = ll;
+        }
+        else {
+            char const * cuu1 (tifo_.getString(Terminfo::properties::CursorUp));
+            while (ll < line_) {
+                write(cuu1);
+                --line_;
+            }
+        }
+    }
+    while (line_ < l) {
+        write("\n");
+        write(tifo_.getString(Terminfo::properties::ClrEol));
+        ++displayHeight_;
+        ++line_;
+    }
+    write('\r');
+    column_ = 0;
+}
+
+prefix_ void senf::term::BaseEditor::reset()
+{
+    for (unsigned i (1); i < displayHeight_; ++i) {
+        toLine(i);
+        clearLine();
+    }
+    toLine(0);
+    displayHeight_ = 1;
+}
+
 prefix_ unsigned senf::term::BaseEditor::currentColumn()
    const
 {
     return column_;
 }
 
+prefix_ unsigned senf::term::BaseEditor::currentLine()
+    const
+{
+    return line_;
+}
+
 prefix_ bool senf::term::BaseEditor::cb_init()
 {
     try {
@@ -197,6 +257,11 @@ prefix_ unsigned senf::term::BaseEditor::width()
     return terminal_->width();
 }
 
+prefix_ unsigned senf::term::BaseEditor::height()
+{
+    return terminal_->height();
+}
+
 prefix_ void senf::term::BaseEditor::write(char ch)
 {
     terminal_->write(ch);
@@ -394,6 +459,23 @@ prefix_ void senf::term::LineEditor::nextHistory()
     }
 }
 
+prefix_ void senf::term::LineEditor::auxDisplay(int line, std::string const & text)
+{
+    toLine(line+1);
+    clearLine();
+    put(text);
+}
+
+prefix_ unsigned senf::term::LineEditor::maxAuxDisplayHeight()
+{
+    return height()-1;
+}
+
+prefix_ void senf::term::LineEditor::clearAuxDisplay()
+{
+    reset();
+}
+
 prefix_ std::string const & senf::term::LineEditor::text()
 {
     return text_;
@@ -436,6 +518,7 @@ prefix_ bool senf::term::LineEditor::cb_init()
 prefix_ void senf::term::LineEditor::cb_windowSizeChanged()
 {
     BaseEditor::cb_windowSizeChanged();
+    clearAuxDisplay();
     prompt(prompt_);
     gotoChar(point_);
     forceRedisplay();
@@ -445,14 +528,19 @@ prefix_ void senf::term::LineEditor::v_keyReceived(keycode_t key)
 {
     if (! enabled_)
         return;
+    clearAuxDisplay();
     lastKey_ = key;
     KeyMap::iterator i (bindings_.find(key));
     if (i != bindings_.end())
         i->second(*this);
     else if (key >= ' ' && key < 256)
         insert(char(key));
+    if (currentLine() != 0)
+        toLine(0);
     if (redisplayNeeded_)
         forceRedisplay();
+    else
+        toColumn(point_ - displayPos_ + promptWidth_ + 1);
 }
 
 ///////////////////////////////////////////////////////////////////////////
@@ -534,6 +622,76 @@ prefix_ void senf::term::bindings::clearScreen(LineEditor & editor)
     editor.forceRedisplay();
 }
 
+prefix_ void senf::term::bindings::complete(LineEditor & editor, Completer completer)
+{
+    typedef std::vector<std::string> Completions;
+
+    Completions completions;
+    completer(editor, 0, editor.point(), completions);
+    if (completions.empty())
+        return;
+    
+    // Find common start string of all completions
+    unsigned commonStart (completions[0].size());
+    unsigned maxLen (commonStart);
+    for (Completions::const_iterator i (boost::next(completions.begin()));
+         i != completions.end(); ++i) {
+        if (i->size() > maxLen)
+            maxLen = i->size();
+        unsigned n (0u);
+        for (; n < commonStart && n < i->size() && completions[0][n] == (*i)[n]; ++n) ;
+        commonStart = n;
+    }
+
+    // Replace to-be-completed string with the common start string shared by all completions
+    std::string text (editor.text());
+    std::string completion (completions[0].substr(0, commonStart));
+    bool didComplete (false);
+    if (text.substr(0, editor.point()) != completion) {
+        text.erase(0, editor.point());
+        text.insert(0, completion);
+        didComplete = true;
+    }
+
+    // If completion is already unique, make sure completion is followed by a space and place cursor
+    // after that space
+    if (completions.size() == 1u) {
+        if (text.size() <= commonStart || text[commonStart] != ' ')
+            text.insert(commonStart, " ");
+        editor.set(text, commonStart + 1);
+        return;
+    }
+
+    // Otherwise place cursor directly after the partial completion
+    editor.set(text, commonStart);
+    if (didComplete)
+        return;
+
+    // Text was not changed, show list of possible completions
+    unsigned colWidth (maxLen+2);
+    unsigned nColumns ((editor.width()-1) / colWidth);
+    if (nColumns < 1) nColumns = 1;
+    unsigned nRows ((completions.size()+nColumns-1) / nColumns);
+    if (nRows > editor.maxAuxDisplayHeight()) {
+        editor.auxDisplay(0, "(too many completions)");
+        return;
+    }
+    Completions::iterator i (completions.begin());
+    for (unsigned row (0); row < nRows; ++row) {
+        std::string line;
+        for (unsigned column (0); column < nColumns && i != completions.end(); ++column) {
+            std::string entry (colWidth, ' ');
+            if (i->size() > colWidth-2)
+                std::copy(i->begin(), i->begin()+colWidth-2, entry.begin());
+            else
+                std::copy(i->begin(), i->end(), entry.begin());
+            line += entry;
+            ++i;
+        }
+        editor.auxDisplay(row, line);
+    }
+}
+
 ///////////////////////////////cc.e////////////////////////////////////////
 #undef prefix_
 //#include "Editor.mpp"
index d654a35..d2d091c 100644 (file)
@@ -54,14 +54,19 @@ namespace term {
         void newline();                 ///< Move to beginning of a new, empty line
         void toColumn(unsigned c);      ///< Move cursor to column \p c
         void put(char ch);              ///< Write \p ch at current column
-        void put(std::string const & text);
+        void put(std::string const & text); ///< Write \a text starting at current column
         void clearLine();               ///< Clear current line and move cursor to first column
         void setBold();                 ///< Set bold char display
         void setNormal();               ///< Set normal char display
         void maybeClrScr();             ///< Clear screen if possible
 
+        void toLine(unsigned l);        ///< Move to relative display line \a l
+        void reset();                   ///< Reset display area to single line
+
         unsigned currentColumn() const; ///< Return number of current column
-        unsigned width();
+        unsigned currentLine() const;   ///< Return number of current relative line
+        unsigned width();               ///< Return current screen width
+        unsigned height();              ///< Return current screen height
 
     protected:
         virtual bool cb_init();
@@ -85,6 +90,8 @@ namespace term {
         ClockService::clock_type keyTimeout_;
         scheduler::TimerEvent timer_;
         unsigned column_;
+        unsigned displayHeight_;
+        unsigned line_;
     };
 
     class LineEditor
@@ -130,6 +137,11 @@ namespace term {
         void prevHistory();
         void nextHistory();
 
+        // Aux Display
+        void auxDisplay(int line, std::string const & text);
+        unsigned maxAuxDisplayHeight();
+        void clearAuxDisplay();
+
         // Get information
         std::string const & text();
         unsigned point();
@@ -179,6 +191,9 @@ namespace bindings {
     void nextHistory         (LineEditor & editor);
     void clearScreen         (LineEditor & editor);
 
+    typedef boost::function<void (LineEditor &, unsigned b, unsigned e, std::vector<std::string> &)> Completer;
+    void complete            (LineEditor & editor, Completer completer);
+
 }
 
 }}
index a746626..12e24b5 100644 (file)
--- a/config.hh
+++ b/config.hh
@@ -93,6 +93,10 @@ namespace config {
 #     define SENF_CONSOLE_MAX_COMMAND_ARITY 6
 # endif
 # 
+# ifndef PHOENIX_LIMIT
+#     define PHOENIX_LIMIT 6
+# endif
+# 
 # ///////////////////////////////hh.e////////////////////////////////////////
 # endif