From 6d2d414e97a8bea6e04db48315a8b26a5da5500e Mon Sep 17 00:00:00 2001 From: Tim Stack Date: Thu, 28 Mar 2024 22:14:18 -0700 Subject: [PATCH] [prql] add more online help --- docs/source/conf.py | 2 +- release/Makefile | 2 +- src/help_text.hh | 14 ++++ src/help_text_formatter.cc | 66 +++++++++++++++++- src/readline_callbacks.cc | 10 ++- src/sql_commands.cc | 20 ++++++ src/sqlite-extension-func.cc | 127 ++++++++++++++++++++++++++++++++++- src/view_helpers.cc | 9 ++- 8 files changed, 239 insertions(+), 11 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6d901c0e..4f94dba7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -258,7 +258,7 @@ copyright = u'2023, Tim Stack' # The short X.Y version. version = '0.12' # The full version, including alpha/beta/rc tags. -release = '0.12.0' +release = '0.12.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/release/Makefile b/release/Makefile index b344cc9b..05358d52 100644 --- a/release/Makefile +++ b/release/Makefile @@ -95,7 +95,7 @@ release: osx-package musl-package release-NEWS.md push: env LANG=UTF-8 package_cloud push tstack/lnav/ubuntu/lucid outbox/lnav*.deb - env LANG=UTF-8 package_cloud push tstack/lnav/el/5 outbox/lnav-0.12.0-1.x86_64.rpm + env LANG=UTF-8 package_cloud push tstack/lnav/el/5 outbox/lnav-0.12.1-1.x86_64.rpm clean: cd vagrant-static && vagrant destroy -f diff --git a/src/help_text.hh b/src/help_text.hh index 68c844a5..5402dfc2 100644 --- a/src/help_text.hh +++ b/src/help_text.hh @@ -45,6 +45,7 @@ enum class help_context_t { HC_SQL_FUNCTION, HC_SQL_TABLE_VALUED_FUNCTION, HC_PRQL_TRANSFORM, + HC_PRQL_FUNCTION, }; enum class help_function_type_t { @@ -97,6 +98,7 @@ struct help_text { std::vector ht_opposites; help_function_type_t ht_function_type{help_function_type_t::HFT_REGULAR}; std::vector ht_prql_path; + const char* ht_default_value{nullptr}; void* ht_impl{nullptr}; help_text() = default; @@ -159,6 +161,12 @@ struct help_text { return *this; } + help_text& prql_function() noexcept + { + this->ht_context = help_context_t::HC_PRQL_FUNCTION; + return *this; + } + help_text& with_summary(const char* summary) noexcept { this->ht_summary = summary; @@ -191,6 +199,12 @@ struct help_text { help_text& with_example(const help_example& example) noexcept; + help_text& with_default_value(const char* defval) + { + this->ht_default_value = defval; + return *this; + } + help_text& optional() noexcept { this->ht_nargs = help_nargs_t::HN_OPTIONAL; diff --git a/src/help_text_formatter.cc b/src/help_text_formatter.cc index 0c20115d..015e92bc 100644 --- a/src/help_text_formatter.cc +++ b/src/help_text_formatter.cc @@ -357,12 +357,55 @@ format_help_text_for_term(const help_text& ht, for (const auto& param : ht.ht_parameters) { out.append(" "); if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { - out.append("["); + out.append(lnav::roles::symbol(param.ht_name)); + out.append(":"); + if (param.ht_default_value) { + out.append(param.ht_default_value); + } else { + out.append("null"); + } + } else { + if (param.ht_group_start) { + out.append(param.ht_group_start); + } + out.append(lnav::roles::variable(param.ht_name)); } - out.append(lnav::roles::variable(param.ht_name)); - if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) { + if (param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) { + out.append("1"_variable); + out.append(" ["); + out.append("..."_variable); + out.append(" "); + out.append(lnav::roles::variable(param.ht_name)); + out.append("N"_variable); out.append("]"); } + if (param.ht_group_end) { + out.append(param.ht_group_end); + } + } + out.with_attr(string_attr{ + line_range{(int) line_start, (int) out.get_string().length()}, + VC_ROLE.value(role_t::VCR_H3), + }); + if (htc != help_text_content::synopsis) { + alb.append("\n") + .append(lnav::roles::table_border( + repeat("\u2550", tws.tws_width))) + .append("\n") + .indent(body_indent) + .append(attr_line_t::from_ansi_str(ht.ht_summary), + &tws.with_indent(body_indent + 2)) + .append("\n"); + } + break; + } + case help_context_t::HC_PRQL_FUNCTION: { + auto line_start = out.al_string.length(); + + out.append(lnav::roles::symbol(ht.ht_name)); + for (const auto& param : ht.ht_parameters) { + out.append(" "); + out.append(lnav::roles::variable(param.ht_name)); if (param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) { out.append("1"_variable); out.append(" ["); @@ -417,6 +460,21 @@ format_help_text_for_term(const help_text& ht, .append(attr_line_t::from_ansi_str(param.ht_summary), &(tws.with_indent(2 + max_param_name_width + 3))) .append("\n"); + if (!param.ht_enum_values.empty()) { + alb.indent(body_indent + max_param_name_width) + .append(" ") + .append("Values"_h5) + .append(": "); + auto initial = true; + for (const auto* ename : param.ht_enum_values) { + if (!initial) { + alb.append("|"); + } + alb.append(lnav::roles::symbol(ename)); + initial = false; + } + alb.append("\n"); + } if (!param.ht_parameters.empty()) { for (const auto& sub_param : param.ht_parameters) { alb.indent(body_indent + max_param_name_width + 3) @@ -536,6 +594,7 @@ format_example_text_for_term(const help_text& ht, case help_context_t::HC_SQL_FUNCTION: case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: case help_context_t::HC_PRQL_TRANSFORM: + case help_context_t::HC_PRQL_FUNCTION: readline_sqlite_highlighter(ex_line, 0); prompt = ";"; break; @@ -627,6 +686,7 @@ format_help_text_for_rst(const help_text& ht, prefix = ""; break; case help_context_t::HC_PRQL_TRANSFORM: + case help_context_t::HC_PRQL_FUNCTION: is_sql = true; break; default: diff --git a/src/readline_callbacks.cc b/src/readline_callbacks.cc index 5c561be0..3eed553d 100644 --- a/src/readline_callbacks.cc +++ b/src/readline_callbacks.cc @@ -140,7 +140,7 @@ const char *PRQL_HELP = const char *PRQL_EXAMPLE = ANSI_UNDERLINE("Examples") "\n" - " from %s | count_by { log_level }\n" + " from %s | stats.count_by { log_level }\n" " from %s | filter log_line == lnav.view.top_line\n" ; @@ -577,7 +577,9 @@ rl_search_internal(readline_curses* rc, ln_mode_t mode, bool complete = false) riter != curr_stage_prql.get_attrs().rend(); ++riter) { - if (riter->sa_type != &lnav::sql::PRQL_PIPE_ATTR) { + if (riter->sa_type != &lnav::sql::PRQL_STAGE_ATTR + || riter->sa_range.lr_start == 0) + { continue; } curr_stage_prql.insert(riter->sa_range.lr_start, @@ -600,7 +602,9 @@ rl_search_internal(readline_curses* rc, ln_mode_t mode, bool complete = false) riter != prev_stage_prql.get_attrs().rend(); ++riter) { - if (riter->sa_type != &lnav::sql::PRQL_PIPE_ATTR) { + if (riter->sa_type != &lnav::sql::PRQL_STAGE_ATTR + || riter->sa_range.lr_start == 0) + { continue; } prev_stage_prql.insert(riter->sa_range.lr_start, diff --git a/src/sql_commands.cc b/src/sql_commands.cc index 2de60dfc..bacaf771 100644 --- a/src/sql_commands.cc +++ b/src/sql_commands.cc @@ -589,6 +589,7 @@ static readline_context::command_t sql_commands[] = { .with_parameter( help_text{"side", "Specifies which rows to include"} .with_enum_values({"inner", "left", "right", "full"}) + .with_default_value("inner") .optional()) .with_parameter( {"table", "The other table to join with the current rows"}) @@ -622,6 +623,25 @@ static readline_context::command_t sql_commands[] = { "prql-source", {"prql-source"}, }, + { + "stats.count_by", + prql_cmd_sort, + help_text( + "stats.count_by", + "Partition rows and count the number of rows in each partition") + .prql_function() + .with_parameter(help_text{"column", "The columns to group by"} + .one_or_more() + .with_grouping("{", "}")) + .with_example({ + "To count rows for a particular value of column 'a'", + "from [{a=1}, {a=1}, {a=2}] | stats.count_by a", + help_example::language::prql, + }), + nullptr, + "prql-source", + {"prql-source"}, + }, { "sort", prql_cmd_sort, diff --git a/src/sqlite-extension-func.cc b/src/sqlite-extension-func.cc index 6e08b71c..f6c5af58 100644 --- a/src/sqlite-extension-func.cc +++ b/src/sqlite-extension-func.cc @@ -1045,13 +1045,136 @@ register_sqlite_funcs(sqlite3* db, sqlite_registration_func_t* reg_funcs) .with_tags({"json"}) .with_example( {"To iterate over an array", - R"(SELECT key,value,type,atom,fullkey,path FROM json_tree('[null,1,"two",{"three":4.5}]'))"}) + R"(SELECT key,value,type,atom,fullkey,path FROM json_tree('[null,1,"two",{"three":4.5}]'))"}), + help_text("text.contains", "Returns true if col contains sub") + .prql_function() + .with_parameter( + help_text{"sub", "The substring to look for in col"}) + .with_parameter(help_text{"col", "The string to examine"}) + .with_example({ + "To check if 'Hello' contains 'lo'", + "from [{s='Hello'}] | select { s=text.contains 'lo' s }", + help_example::language::prql, + }) + .with_example({ + "To check if 'Goodbye' contains 'lo'", + "from [{s='Goodbye'}] | select { s=text.contains 'lo' s }", + help_example::language::prql, + }), + help_text("text.ends_with", "Returns true if col ends with suffix") + .prql_function() + .with_parameter( + help_text{"suffix", "The string to look for at the end of col"}) + .with_parameter(help_text{"col", "The string to examine"}) + .with_example({ + "To check if 'Hello' ends with 'lo'", + "from [{s='Hello'}] | select { s=text.ends_with 'lo' s }", + help_example::language::prql, + }) + .with_example({ + "To check if 'Goodbye' ends with 'lo'", + "from [{s='Goodbye'}] | select { s=text.ends_with 'lo' s }", + help_example::language::prql, + }), + help_text("text.extract", "Extract a slice of a string") + .prql_function() + .with_parameter(help_text{ + "idx", + "The starting index where the first character is index 1"}) + .with_parameter(help_text{"len", "The length of the slice"}) + .with_parameter(help_text{"str", "The string to extract from"}) + .with_example({ + "To extract a substring from s", + "from [{s='Hello, World!'}] | select { s=text.extract 1 5 s }", + help_example::language::prql, + }), + help_text("text.length", "Returns the number of characters in col") + .prql_function() + .with_parameter(help_text{"col", "The string to examine"}) + .with_example({ + "To count the number of characters in s", + "from [{s='Hello, World!'}] | select { s=text.length s }", + help_example::language::prql, + }), + help_text("text.lower", "Converts col to lowercase") + .prql_function() + .with_parameter(help_text{"col", "The string to convert"}) + .with_example({ + "To convert s to lowercase", + "from [{s='HELLO'}] | select { s=text.lower s }", + help_example::language::prql, + }), + help_text("text.ltrim", "Remove whitespace from the left side of col") + .prql_function() + .with_parameter(help_text{"col", "The string to trim"}) + .with_example({ + "To trim the left side of s", + "from [{s=' HELLO '}] | select { s=text.ltrim s }", + help_example::language::prql, + }), + help_text("text.replace", + "Replace all occurrences of before with after in col") + .prql_function() + .with_parameter(help_text{"before", "The string to find"}) + .with_parameter(help_text{"after", "The replacement"}) + .with_parameter(help_text{"col", "The string to trim"}) + .with_example({ + "To erase foo in s", + "from [{s='foobar'}] | select { s=text.replace 'foo' '' s }", + help_example::language::prql, + }), + help_text("text.rtrim", "Remove whitespace from the right side of col") + .prql_function() + .with_parameter(help_text{"col", "The string to trim"}) + .with_example({ + "To trim the right side of s", + "from [{s=' HELLO '}] | select { s=text.rtrim s }", + help_example::language::prql, + }), + help_text("text.starts_with", "Returns true if col starts with suffix") + .prql_function() + .with_parameter(help_text{ + "suffix", "The string to look for at the start of col"}) + .with_parameter(help_text{"col", "The string to examine"}) + .with_example({ + "To check if 'Hello' starts with 'lo'", + "from [{s='Hello'}] | select { s=text.starts_with 'He' s }", + help_example::language::prql, + }) + .with_example({ + "To check if 'Goodbye' starts with 'lo'", + "from [{s='Goodbye'}] | select { s=text.starts_with 'He' s }", + help_example::language::prql, + }), + help_text("text.trim", "Remove whitespace from the both sides of col") + .prql_function() + .with_parameter(help_text{"col", "The string to trim"}) + .with_example({ + "To trim s", + "from [{s=' HELLO '}] | select { s=text.trim s }", + help_example::language::prql, + }), + help_text("text.upper", "Converts col to uppercase") + .prql_function() + .with_parameter(help_text{"col", "The string to convert"}) + .with_example({ + "To convert s to uppercase", + "from [{s='hello'}] | select { s=text.upper s }", + help_example::language::prql, + }), }; if (!help_registration_done) { for (auto& ht : builtin_funcs) { - sqlite_function_help.insert(std::make_pair(ht.ht_name, &ht)); + switch (ht.ht_context) { + case help_context_t::HC_PRQL_FUNCTION: + lnav::sql::prql_functions.emplace(ht.ht_name, &ht); + break; + default: + sqlite_function_help.emplace(ht.ht_name, &ht); + break; + } ht.index_tags(); } } diff --git a/src/view_helpers.cc b/src/view_helpers.cc index db37585d..2185c504 100644 --- a/src/view_helpers.cc +++ b/src/view_helpers.cc @@ -952,7 +952,8 @@ execute_example(const help_text& ht) case help_context_t::HC_SQL_INFIX: case help_context_t::HC_SQL_FUNCTION: case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: - case help_context_t::HC_PRQL_TRANSFORM: { + case help_context_t::HC_PRQL_TRANSFORM: + case help_context_t::HC_PRQL_FUNCTION: { exec_context ec; ec.ec_label_source_stack.push_back(&dls); @@ -1010,6 +1011,12 @@ execute_examples() for (auto help_pair : sqlite_function_help) { execute_example(*help_pair.second); } + for (auto help_pair : lnav::sql::prql_functions) { + if (help_pair.second->ht_context != help_context_t::HC_PRQL_FUNCTION) { + continue; + } + execute_example(*help_pair.second); + } for (auto cmd_pair : *sql_cmd_map) { if (cmd_pair.second->c_help.ht_context != help_context_t::HC_PRQL_TRANSFORM)