diff options
Diffstat (limited to 'src/help.c')
-rw-r--r-- | src/help.c | 389 |
1 files changed, 389 insertions, 0 deletions
diff --git a/src/help.c b/src/help.c new file mode 100644 index 0000000..fe6df1d --- /dev/null +++ b/src/help.c @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2012-2021 Robin Haberkorn + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <glib/gprintf.h> + +#include "sciteco.h" +#include "string-utils.h" +#include "error.h" +#include "parser.h" +#include "core-commands.h" +#include "qreg.h" +#include "ring.h" +#include "interface.h" +#include "rb3str.h" +#include "help.h" + +static void teco_help_set(const gchar *topic_name, const gchar *filename, teco_int_t pos); + +static GStringChunk *teco_help_chunk = NULL; + +/** @extends teco_rb3str_head_t */ +typedef struct { + teco_rb3str_head_t head; + + teco_int_t pos; + gchar filename[]; +} teco_help_topic_t; + +/** @static @memberof teco_help_topic_t */ +static teco_help_topic_t * +teco_help_topic_new(const gchar *topic_name, const gchar *filename, teco_int_t pos) +{ + /* + * Topics are inserted only once into the RB tree, so we can store + * the strings in a GStringChunk. + * + * FIXME: The same should be true for teco_help_topic_t object itself. + * It could be allocated via a stack allocator. + */ + teco_help_topic_t *topic = g_malloc0(sizeof(teco_help_topic_t) + strlen(filename) + 1); + teco_string_init_chunk(&topic->head.name, topic_name, strlen(topic_name), teco_help_chunk); + topic->pos = pos; + strcpy(topic->filename, filename); + return topic; +} + +/** @memberof teco_help_topic_t */ +static inline void +teco_help_topic_free(teco_help_topic_t *ctx) +{ + /* + * NOTE: The topic name is allocated via GStringChunk and can only be + * be deallocated together. + */ + g_free(ctx); +} + +static teco_rb3str_tree_t teco_help_tree; + +static gboolean +teco_help_init(GError **error) +{ + if (G_LIKELY(teco_help_chunk != NULL)) + /* already loaded */ + return TRUE; + + teco_help_chunk = g_string_chunk_new(32); + rb3_reset_tree(&teco_help_tree); + + teco_qreg_t *lib_reg = teco_qreg_table_find(&teco_qreg_table_globals, "$SCITECOPATH", 12); + g_assert(lib_reg != NULL); + g_auto(teco_string_t) lib_path = {NULL, 0}; + if (!lib_reg->vtable->get_string(lib_reg, &lib_path.data, &lib_path.len, error)) + return FALSE; + /* + * FIXME: lib_path may contain null-bytes. + * It's not clear how to deal with this. + */ + g_autofree gchar *women_path = g_build_filename(lib_path.data, "women", NULL); + + /* + * FIXME: We might want to gracefully handle only the G_FILE_ERROR_NOENT + * error and propagate all other errors? + */ + g_autoptr(GDir) women_dir = g_dir_open(women_path, 0, NULL); + if (!women_dir) + return TRUE; + + const gchar *basename; + while ((basename = g_dir_read_name(women_dir))) { + if (!g_str_has_suffix(basename, ".woman")) + continue; + + /* + * Open the corresponding SciTECO macro to read + * its first line. + */ + g_autofree gchar *filename = g_build_filename(women_path, basename, NULL); + g_autofree gchar *filename_tec = g_strconcat(filename, ".tec", NULL); + g_autoptr(FILE) file = g_fopen(filename_tec, "r"); + if (!file) { + /* + * There might simply be no support script for + * simple plain-text woman-pages. + * In this case we create a topic using the filename + * without an extension. + */ + g_autofree gchar *topic = g_strndup(basename, strlen(basename)-6); + teco_help_set(topic, filename, 0); + continue; + } + + /* + * Each womanpage script begins with a special comment + * header containing the position to topic index. + * Every topic will be on its own line and they are unlikely + * to be very long, so we can use fgets() here. + * + * NOTE: Since we haven't opened with the "b" flag, + * fgets() will translate linebreaks to LF even on + * MSVCRT (Windows). + */ + gchar buffer[1024]; + if (!fgets(buffer, sizeof(buffer), file) || + !g_str_has_prefix(buffer, "!*")) { + teco_interface_msg(TECO_MSG_WARNING, + "Missing or invalid topic line in womanpage script \"%s\"", + filename); + continue; + } + /* skip opening comment */ + gchar *topic = buffer+2; + + do { + gchar *endptr; + teco_int_t pos = strtoul(topic, &endptr, 10); + + /* + * This also breaks at the last line of the + * header. + */ + if (*endptr != ':') + break; + + /* + * Strip the likely LF at the end of the line. + */ + gsize len = strlen(endptr)-1; + if (G_LIKELY(endptr[len] == '\n')) + endptr[len] = '\0'; + + teco_help_set(endptr+1, filename, pos); + } while ((topic = fgets(buffer, sizeof(buffer), file))); + } + + return TRUE; +} + +static inline teco_help_topic_t * +teco_help_find(const gchar *topic_name) +{ + /* + * The topic index contains printable characters + * only (to avoid having to perform string building + * on the topic terms to be able to define control + * characters). + * Therefore, we expand control characters in the + * look-up string to their printable forms. + */ + g_autofree gchar *term = teco_string_echo(topic_name, strlen(topic_name)); + return (teco_help_topic_t *)teco_rb3str_find(&teco_help_tree, FALSE, term, strlen(term)); +} + +static void +teco_help_set(const gchar *topic_name, const gchar *filename, teco_int_t pos) +{ + teco_help_topic_t *topic; + teco_help_topic_t *existing = teco_help_find(topic_name); + if (existing) { + if (!strcmp(existing->filename, filename)) { + /* + * A topic with the same name already exists + * in the same file. + * For the time being, we simply overwrite the + * last topic. + * FIXME: Perhaps make it unique again!? + */ + existing->pos = pos; + return; + } + + /* in another file -> make name unique */ + teco_interface_msg(TECO_MSG_WARNING, + "Topic collision: \"%s\" defined in \"%s\" and \"%s\"", + topic_name, existing->filename, filename); + + g_autofree gchar *basename = g_path_get_basename(filename); + g_autofree gchar *unique_name = g_strconcat(topic_name, ":", basename, NULL); + topic = teco_help_topic_new(unique_name, filename, pos); + } else { + topic = teco_help_topic_new(topic_name, filename, pos); + } + + teco_rb3str_insert(&teco_help_tree, FALSE, &topic->head); +} + +gboolean +teco_help_auto_complete(const gchar *topic_name, teco_string_t *insert) +{ + return teco_rb3str_auto_complete(&teco_help_tree, FALSE, topic_name, + topic_name ? strlen(topic_name) : 0, 0, insert); +} + +#ifndef NDEBUG +static void __attribute__((destructor)) +teco_help_cleanup(void) +{ + if (!teco_help_chunk) + /* not initialized */ + return; + g_string_chunk_free(teco_help_chunk); + + struct rb3_head *cur; + + while ((cur = rb3_get_root(&teco_help_tree))) { + rb3_unlink_and_rebalance(cur); + teco_help_topic_free((teco_help_topic_t *)cur); + } +} +#endif + +/* + * Command states + */ + +static gboolean +teco_state_help_initial(teco_machine_main_t *ctx, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return TRUE; + + /* + * The help-index is populated on demand, + * so we start up quicker and batch mode does + * not depend on the availability of the standard + * library. + */ + return teco_help_init(error); +} + +static teco_state_t * +teco_state_help_done(teco_machine_main_t *ctx, const teco_string_t *str, GError **error) +{ + if (ctx->mode > TECO_MODE_NORMAL) + return &teco_state_start; + + if (teco_string_contains(str, '\0')) { + g_set_error_literal(error, TECO_ERROR, TECO_ERROR_FAILED, + "Help topic must not contain null-byte"); + return NULL; + } + teco_help_topic_t *topic = teco_help_find(str->data); + if (!topic) { + g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, + "Topic \"%s\" not found", str->data); + return NULL; + } + + teco_ring_undo_edit(); + /* + * ED hooks with the default lexer framework + * will usually load the styling SciTECO script + * when editing the buffer for the first time. + */ + if (!teco_ring_edit(topic->filename, error)) + return NULL; + + /* + * Make sure the topic is visible. + * We do need undo tokens for this (even though + * the buffer is removed on rubout if the woman + * page is viewed first) since we might browse + * multiple topics in the same buffer without + * closing it first. + */ + undo__teco_interface_ssm(SCI_GOTOPOS, + teco_interface_ssm(SCI_GETCURRENTPOS, 0, 0), 0); + teco_interface_ssm(SCI_GOTOPOS, topic->pos, 0); + + return &teco_state_start; +} + +/* in cmdline.c */ +gboolean teco_state_help_process_edit_cmd(teco_machine_main_t *ctx, teco_machine_t *parent_ctx, gchar chr, GError **error); + +/*$ "?" help + * ?[topic]$ -- Get help for topic + * + * Look up <topic> in the help index, opening + * the corresponding womanpage as a buffer and scrolling + * to the topic's position. + * The help index is built when this command is first + * executed, so the help system does not consume resources + * when not used (e.g. in a batch-mode script). + * + * \*(ST's help documents must be installed in the + * directory \fB$SCITECOPATH/women\fP, i.e. as part of + * the standard library. + * Each document consist of at least one plain-text file with + * the extension \(lq.woman\(rq. + * Optionally, a \*(ST script with the extension + * \(lq.woman.tec\(rq can be installed alongside the + * main document to define topics covered by this document + * and set up styling. + * + * The beginning of the script must be a header of the form: + * .EX + * !*\fIposition\fP:\fItopic1\fP + * \fIposition2\fP:\fItopic2\fP + * \fI...\fP + * *! + * .EE + * In other words it must be a \*(ST comment followed + * by an asterisk sign, followed by the first topic which + * is a buffer position, followed by a colon and the topic + * string. + * The topic string is terminated by the end of the line. + * The end of the header is marked by a single \(lq*!\(rq. + * Topic terms should be specified with printable characters + * only (e.g. use Caret+A instead of CTRL+A). + * When looking up a help term, control characters are + * canonicalized to their printable form, so the term + * \(lq^A\(rq is found both by Caret+A and CTRL+A. + * Also, while topic terms are not case folded, lookup + * is case insensitive. + * + * The rest of the script is not read by \*(ST internally + * but should contain styling for the main document. + * It is usually read by the standard library's lexer + * configuration system when showing a womanpage. + * If the \(lq.woman.tec\(rq macro is missing, + * \*(ST will define a single topic for the document based + * on the \(lq.woman\(rq file's name. + * + * The combination of plain-text document and script + * is called a \(lqwomanpage\(rq because these files + * are usually generated using \fBgroff\fP(1) with the + * \fIgrosciteco\fP formatter and the \fIsciteco.tmac\fP + * GNU troff macros. + * When using womanpages generated by \fIgrosciteco\fP, + * help topics can be defined using the \fBTECO_TOPIC\fP + * Troff macro. + * This flexible system allows \*(ST to access internal + * and third-party help files written in plain-text or + * with an arbitrary GNU troff macro package. + * As all GNU troff documents are processed at build-time, + * GNU troff is not required at runtime. + * + * The \fB?\fP command does not have string building enabled. + */ +TECO_DEFINE_STATE_EXPECTSTRING(teco_state_help, + .initial_cb = (teco_state_initial_cb_t)teco_state_help_initial, + .process_edit_cmd_cb = (teco_state_process_edit_cmd_cb_t)teco_state_help_process_edit_cmd, + .expectstring.string_building = FALSE +); |