diff --git a/alib2cli/CMakeLists.txt b/alib2cli/CMakeLists.txt index 4ba76f3ec04e274d196a6e376f5e781066de9970..5107a503523d59451cc47f2ab23dc33af6e08239 100644 --- a/alib2cli/CMakeLists.txt +++ b/alib2cli/CMakeLists.txt @@ -40,7 +40,7 @@ set_source_files_properties( GENERATED TRUE COMPILE_OPTIONS "-w") -target_include_directories(alib2cli PRIVATE ${ANTLR4_INCLUDE_DIR_AltCliGrammarLexer} ${ANTLR4_INCLUDE_DIR_AltCliGrammarParser}) +target_include_directories(alib2cli PUBLIC ${ANTLR4_INCLUDE_DIR_AltCliGrammarLexer} ${ANTLR4_INCLUDE_DIR_AltCliGrammarParser}) target_include_directories(alib2cli SYSTEM PUBLIC ${ANTLR4_INCLUDE_DIR}) target_link_libraries(alib2cli INTERFACE antlr4_shared) diff --git a/alib2cli/src/environment/Environment.cpp b/alib2cli/src/environment/Environment.cpp index d241adaa154553eb8e9f9bb2733b449d0f549206..d40d5c1f7942e3450f7dadae51757d235ff1faeb 100644 --- a/alib2cli/src/environment/Environment.cpp +++ b/alib2cli/src/environment/Environment.cpp @@ -77,4 +77,19 @@ Environment& Environment::getGlobalScope() return m_upper->getGlobalScope(); } +cli::CommandResult Environment::execute(const std::unique_ptr<cli::Command>& ast) +{ + try { + cli::CommandResult res = ast->run(*this); + + if (res == CommandResult::CONTINUE || res == CommandResult::BREAK) + throw std::logic_error("There is no loop to continue/break."); + + return res; + } catch (...) { + alib::ExceptionHandler::handle(common::Streams::err); + return cli::CommandResult::EXCEPTION; + } +} + } /* namespace cli */ diff --git a/alib2cli/src/environment/Environment.h b/alib2cli/src/environment/Environment.h index b4b7eb5014daa460c9236673f1922733efa05c78..0e2d90af5c87738a01eecd473cd77d6192547653 100644 --- a/alib2cli/src/environment/Environment.h +++ b/alib2cli/src/environment/Environment.h @@ -120,6 +120,7 @@ public: } cli::CommandResult execute(const std::shared_ptr<cli::LineInterface>& lineInterface); + cli::CommandResult execute(const std::unique_ptr<cli::Command>& ast); cli::CommandResult execute_line(cli::CharSequence charSequence); diff --git a/alib2cli/src/parser/Parser2.cpp b/alib2cli/src/parser/Parser2.cpp index e3b3985844985dfc683fc073dadc69953da10814..6da9503d831ae9d69b7f2dc1defc4aa2bb592955 100644 --- a/alib2cli/src/parser/Parser2.cpp +++ b/alib2cli/src/parser/Parser2.cpp @@ -28,10 +28,11 @@ std::unique_ptr<CommandList> Parser2::parse(antlr4::CharStream& stream) auto listener = std::make_shared<cli::grammar::BufferedErrorListener>(); AltCliLexer lexer(&stream); - + lexer.removeErrorListeners(); lexer.addErrorListener(listener.get()); antlr4::CommonTokenStream tokenStream(&lexer); AltCliParser parser(&tokenStream); + parser.removeErrorListeners(); parser.addErrorListener(listener.get()); parser.setErrorHandler(std::make_shared<antlr4::DefaultErrorStrategy>()); diff --git a/aql2/CMakeLists.txt b/aql2/CMakeLists.txt index 5a25be108974154b1282b59b3dce6d6161233380..d6929a3d542b817bfcec13c500145033d83e629f 100644 --- a/aql2/CMakeLists.txt +++ b/aql2/CMakeLists.txt @@ -1,13 +1,14 @@ project(alt-aql VERSION ${CMAKE_PROJECT_VERSION}) find_package(LibXml2 REQUIRED) +find_package(replxx REQUIRED) find_package(PkgConfig) pkg_check_modules(TCLAP REQUIRED IMPORTED_TARGET tclap>=1.2.5) pkg_check_modules(READLINE REQUIRED IMPORTED_TARGET readline>=8.0) alt_executable(aql2 - DEPENDS alib2elgo alib2graph_algo alib2algo alib2aux alib2raw_cli_integration alib2raw alib2str_cli_integration alib2str alib2graph_data alib2data alib2cli alib2xml alib2common alib2abstraction alib2measure alib2std LibXml2::LibXml2 PkgConfig::READLINE PkgConfig::TCLAP + DEPENDS alib2elgo alib2graph_algo alib2algo alib2aux alib2raw_cli_integration alib2raw alib2str_cli_integration alib2str alib2graph_data alib2data alib2cli alib2xml alib2common alib2abstraction alib2measure alib2std LibXml2::LibXml2 PkgConfig::READLINE PkgConfig::TCLAP replxx ) # stdlib diff --git a/aql2/src/REPL.cpp b/aql2/src/REPL.cpp new file mode 100644 index 0000000000000000000000000000000000000000..61b91485af9d2fc4d21725d99551f086bf8412b7 --- /dev/null +++ b/aql2/src/REPL.cpp @@ -0,0 +1,123 @@ +#include <AltCliLexer.h> +#include <functional> +#include <grammar/Autocomplete.h> +#include <parser/Parser2.h> +#include <replxx.hxx> +#include "REPL.h" + +using namespace replxx; +using namespace replxx::helper; + +cli::CommandResult REPL::run(cli::Environment& env) +{ + using cl = Replxx::Color; + syntax_highlight_t regex_color{ + // numbers + {"[\\-|+]{0,1}[0-9]+", cl::BLUE}, // integers + {"[\\-|+]{0,1}[0-9]*\\.[0-9]+", cl::BLUE}, // decimals + {"[\\-|+]{0,1}[0-9]+e[\\-|+]{0,1}[0-9]+", cl::BLUE}, // scientific notation + + // strings + {"\"(.|\n)*?\"", cl::YELLOW}, // double quotes + }; + + Replxx rx; + // set the max history size + rx.set_max_history_size(128); + // set the max number of hint rows to show + rx.set_max_hint_rows(3); + rx.set_word_break_characters(" \n\t.,-%!;:=*~^'\"/?<>|[](){}"); + rx.set_completion_count_cutoff(128); + rx.set_double_tab_completion(false); + rx.set_complete_on_empty(true); + rx.set_beep_on_ambiguous_completion(false); + rx.set_no_color(false); + rx.set_indent_multiline(true); + + rx.set_highlighter_callback([regex_color, this](std::string const& context, replxx::Replxx::colors_t& colors) { + return hook_color(context, colors, regex_color, this->m_tokens); + }); + std::vector<std::string> lines; + + rx.bind_key(Replxx::KEY::ENTER, [&](char32_t) { + std::string input{rx.get_state().text()}; + if (input.empty()) { + return rx.invoke(replxx::Replxx::ACTION::COMMIT_LINE, Replxx::KEY::ENTER); + } + try { + cli::Parser2::parseString(input); + } catch (const std::exception&) { + return rx.invoke(Replxx::ACTION::NEW_LINE, Replxx::KEY::ENTER); + } + + return rx.invoke(replxx::Replxx::ACTION::COMMIT_LINE, Replxx::KEY::ENTER); + }); + + cli::Autocomplete complete; + + rx.set_completion_callback([&complete](const std::string&, int&) { + Replxx::completions_t completions; + + return completions; + }); + + std::string prompt = "aql> "; + + + // display the prompt and retrieve input from the user + char const* cInput; + while (true) { + do { + cInput = rx.input(prompt); + } while ((cInput == nullptr) && (errno == EAGAIN)); + + if (cInput == nullptr) { + break; + } + + // change cInput into a std::string + // easier to manipulate + std::string input{cInput}; + if (input.empty()) { + continue; + } + + std::unique_ptr<cli::Command> ast = cli::Parser2::parseString(input); + env.execute(ast); + + + rx.history_add(input); + } + return cli::CommandResult::OK; +} + +REPL::REPL() +{ + antlr4::ANTLRInputStream inputStream{""}; + cli::grammar::lexer::AltCliLexer const lexer{&inputStream}; + + const auto& vocabulary = lexer.getVocabulary(); + + const auto tokensCount = vocabulary.getMaxTokenType(); + + for (size_t i = 0; i < tokensCount; ++i) { + auto token = vocabulary.getDisplayName(i); + + if (token == vocabulary.getSymbolicName(i)) { + continue; + } + + if (token[0] == '\'' && token[token.size() - 1] == '\'') { + token = token.substr(1, token.size() - 2); + } + + const bool isKeyword = std::all_of(token.cbegin(), token.cend(), [](char c) { + return isalpha(c); + }); + + if (isKeyword) + m_tokens.emplace(token, replxx::Replxx::Color::BRIGHTBLUE); + else + m_tokens.emplace(token, replxx::Replxx::Color::GREEN); + } +} diff --git a/aql2/src/REPL.h b/aql2/src/REPL.h new file mode 100644 index 0000000000000000000000000000000000000000..32c9cd3b47fd031420e04f217134051f44b46702 --- /dev/null +++ b/aql2/src/REPL.h @@ -0,0 +1,27 @@ +#pragma once + +#include <string> +#include <vector> + +#include <environment/Environment.h> +#include <replxx.hxx> + +class REPL { + std::unordered_map<std::string, replxx::Replxx::Color> m_tokens; +public: + REPL(); + + cli::CommandResult run(cli::Environment& environment); +}; + +namespace replxx::helper { + int utf8str_codepoint_len(char const *s, int utf8len); + int context_len(char const *prefix); + + typedef std::vector<std::pair<std::string, Replxx::Color>> syntax_highlight_t; + typedef std::unordered_map<std::string, Replxx::Color> keyword_highlight_t; + + + void hook_color(std::string const &context, replxx::Replxx::colors_t &colors, + syntax_highlight_t const ®ex_color, keyword_highlight_t const &word_color); +} \ No newline at end of file diff --git a/aql2/src/REPL.replxx.cpp b/aql2/src/REPL.replxx.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c98bfad92cb9848463ed73a4cf00aef8f92fdb1e --- /dev/null +++ b/aql2/src/REPL.replxx.cpp @@ -0,0 +1,132 @@ +#include "REPL.h" + +#include <cstring> +#include <functional> +#include <regex> + +using namespace replxx; + +namespace replxx::helper { + +int utf8str_codepoint_len(char const* s, int utf8len) +{ + int codepointLen = 0; + unsigned char m4 = 128 + 64 + 32 + 16; + unsigned char m3 = 128 + 64 + 32; + unsigned char m2 = 128 + 64; + for (int i = 0; i < utf8len; ++i, ++codepointLen) { + char c = s[i]; + if ((c & m4) == m4) { + i += 3; + } else if ((c & m3) == m3) { + i += 2; + } else if ((c & m2) == m2) { + i += 1; + } + } + return (codepointLen); +} + +int context_len(char const* prefix) +{ + auto wb = " \t\n\r\v\f-=+*&^%$#@!,./?<>;:`~'\"[]{}()\\|"; + int i = static_cast<int>(strlen(prefix) - 1); + int cl = 0; + while (i >= 0) { + if (strchr(wb, prefix[i]) != NULL) { + break; + } + ++cl; + --i; + } + return (cl); +} + +inline bool is_kw(char ch) +{ + return isalnum(ch) || (ch == '_'); +} + +void hook_color(std::string const& context, replxx::Replxx::colors_t& colors, syntax_highlight_t const& regex_color, keyword_highlight_t const& word_color) +{ + + bool inWord(false); + int wordStart(0); + int wordEnd(0); + int colorOffset(0); + auto dohl = [&](int i) { + inWord = false; + std::string intermission(context.substr(wordEnd, wordStart - wordEnd)); + colorOffset += utf8str_codepoint_len(intermission.c_str(), intermission.length()); + int wordLen(i - wordStart); + std::string keyword(context.substr(wordStart, wordLen)); + bool bold(false); + if (keyword.substr(0, 5) == "bold_") { + keyword = keyword.substr(5); + bold = true; + } + bool underline(false); + if (keyword.substr(0, 10) == "underline_") { + keyword = keyword.substr(10); + underline = true; + } + keyword_highlight_t::const_iterator it(word_color.find(keyword)); + Replxx::Color color = Replxx::Color::DEFAULT; + if (it != word_color.end()) { + color = it->second; + } + if (bold) { + color = replxx::color::bold(color); + } + if (underline) { + color = replxx::color::underline(color); + } + for (int k(0); k < wordLen; ++k) { + Replxx::Color& c(colors.at(colorOffset + k)); + if (color != Replxx::Color::DEFAULT) { + c = color; + } + } + colorOffset += wordLen; + wordEnd = i; + }; + for (int i(0); i < static_cast<int>(context.length()); ++i) { + if (!inWord) { + if (is_kw(context[i])) { + inWord = true; + wordStart = i; + } + } else if (inWord && !is_kw(context[i])) { + dohl(i); + } + if ((context[i] != '_') && ispunct(context[i])) { + wordStart = i; + dohl(i + 1); + } + } + if (inWord) { + dohl(context.length()); + } + + // highlight matching regex sequences + for (auto const& e : regex_color) { + size_t pos{0}; + std::string str = context; + std::smatch match; + + while (std::regex_search(str, match, std::regex(e.first))) { + std::string c{match[0]}; + std::string prefix(match.prefix().str()); + pos += utf8str_codepoint_len(prefix.c_str(), static_cast<int>(prefix.length())); + int len(utf8str_codepoint_len(c.c_str(), static_cast<int>(c.length()))); + + for (int i = 0; i < len; ++i) { + colors.at(pos + i) = e.second; + } + + pos += len; + str = match.suffix(); + } + } +} +}