/* * Copyright (C) 2012-2026 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #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, gsize pos); /** @extends teco_rb3str_head_t */ typedef struct { teco_rb3str_head_t head; /** position of topic in filename (in bytes) */ gsize pos; gchar filename[]; } teco_help_topic_t; static struct { /** * Search tree for help topics. * Several topics with the same name (key) are allowed. */ teco_rb3str_tree_t tree; /** first entry found for a given topic */ teco_help_topic_t *first; /** last entry shown for a given topic */ teco_help_topic_t *current; GStringChunk *chunk; } teco_help = {0}; /** @static @memberof teco_help_topic_t */ static teco_help_topic_t * teco_help_topic_new(const gchar *topic_name, const gchar *filename, gsize 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 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); g_auto(teco_string_t) lib_path = {NULL, 0}; if (!teco_qreg_table_get_string(&teco_qreg_table_globals, "$SCITECOPATH", &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; gsize 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; } /* * Compare only for the key name (search term), but we are lower than * all of the existing entries. */ static gint teco_help_casecmp_plain(const teco_rb3str_head_t *head, const teco_string_t *data) { return teco_string_casecmp(head->key, data->data, data->len) ? : 1; } 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)); teco_string_t data = {(gchar *)term, strlen(term)}; teco_help_topic_t *topic = (teco_help_topic_t *)rb3_nfind(&teco_help.tree, (rb3_cmp *)teco_help_casecmp_plain, &data); return topic && !teco_string_casecmp(topic->head.key, data.data, data.len) ? topic : NULL; } /* * By turning the filename and position into secondary keys, we can insert * several topics with the same search term and they will be sorted. * Alternatively, we'd have to keep lists in each teco_help_topic_t. */ static gint teco_help_topic_casecmp(const teco_help_topic_t *topic, const teco_help_topic_t *data) { return teco_string_casecmp(topic->head.key, data->head.key.data, data->head.key.len) ? : strcasecmp(topic->filename, data->filename) ? : (gssize)topic->pos - data->pos; } /* * If the topic is already present in the tree, we will still * insert the given topic. */ static inline void teco_help_set(const gchar *topic_name, const gchar *filename, gsize pos) { teco_help_topic_t *topic = teco_help_topic_new(topic_name, filename, pos); if (G_UNLIKELY(rb3_insert(&teco_help.tree, &topic->head.head, (rb3_cmp *)teco_help_topic_casecmp, topic) != NULL)) /* * Should not happen - just keep the old entry. * This will "leak" some memory in the teco_help.chunk, though. */ teco_help_topic_free(topic); } 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); } static void TECO_DEBUG_CLEANUP 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); } } /* * Command states */ static gboolean teco_state_help_initial(teco_machine_main_t *ctx, GError **error) { if (ctx->flags.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, teco_string_t str, GError **error) { if (ctx->flags.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; } if (str.len) { teco_help_topic_t *next = teco_help_find(str.data); if (!next) { g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "Topic \"%s\" not found", str.data); return NULL; } teco_undo_ptr(teco_help.current) = next; teco_undo_ptr(teco_help.first) = next; guint results = 0; for (teco_help_topic_t *cur = teco_help.first; cur && !teco_string_cmp(cur->head.name, teco_help.first->head.name.data, teco_help.first->head.name.len); cur = (teco_help_topic_t *)teco_rb3str_get_next(&cur->head)) results++; if (results > 1) teco_interface_msg(TECO_MSG_INFO, "Found %u help entries", results); } else if (teco_help.current) { /* toggle through topics with the same name */ teco_help_topic_t *next = (teco_help_topic_t *)teco_rb3str_get_next(&teco_help.current->head); if (!next || teco_string_cmp(next->head.name, teco_help.first->head.name.data, teco_help.first->head.name.len) != 0) next = teco_help.first; teco_undo_ptr(teco_help.current) = next; } else { g_set_error(error, TECO_ERROR, TECO_ERROR_FAILED, "No help topic found"); return NULL; } /* * ED hooks with the default lexer framework * will usually load the styling SciTECO script * when editing the buffer for the first time. */ if (!teco_current_doc_undo_edit(error) || !teco_ring_edit(teco_help.current->filename, error)) return NULL; teco_current_doc_set_dot(teco_interface_bytes2glyphs_absdot(teco_help.current->pos)); /* * 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, teco_help.current->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, gunichar chr, GError **error); gboolean teco_state_help_insert_completion(teco_machine_main_t *ctx, teco_string_t str, GError **error); /*$ "?" help * ?[topic]$ -- Get help for topic * * Look up in the help index, opening * the corresponding womanpage as a buffer and scrolling * to the topic's position. * If a is defined in several places, you can * call \(lq?\fB$\fP\(rq to toggle through all of them. * * 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 in bytes, 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 \fBSCITECO_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, .insert_completion_cb = (teco_state_insert_completion_cb_t)teco_state_help_insert_completion, .expectstring.string_building = FALSE, .expectstring.done_cb = teco_state_help_done );