From 2ce7518562b31116feb9bd0cb925838e07f25d40 Mon Sep 17 00:00:00 2001 From: myk002 Date: Thu, 21 Jul 2022 22:33:43 -0700 Subject: [PATCH] read plugin command docs from single plugin file --- library/LuaApi.cpp | 65 ++++++---- library/lua/helpdb.lua | 273 +++++++++++++++++++++++------------------ 2 files changed, 196 insertions(+), 142 deletions(-) diff --git a/library/LuaApi.cpp b/library/LuaApi.cpp index 770aab870..4ee1a4d1c 100644 --- a/library/LuaApi.cpp +++ b/library/LuaApi.cpp @@ -3109,41 +3109,58 @@ static int internal_listCommands(lua_State *L) } return 1; } -static int internal_getCommandHelp(lua_State *L) + +static const PluginCommand * getPluginCommand(const char * command) { auto plugins = Core::getInstance().getPluginManager(); - - const char *name = luaL_checkstring(L, 1); - - auto plugin = plugins->getPluginByCommand(name); + auto plugin = plugins->getPluginByCommand(command); if (!plugin) { - lua_pushnil(L); - return 1; + return NULL; } size_t num_commands = plugin->size(); for (size_t i = 0; i < num_commands; ++i) { - if ((*plugin)[i].name == name) - { - const auto &pc = (*plugin)[i]; - std::string help = pc.description; - if (help.size() && help[help.size()-1] != '.') - { - help += "."; - } - if (pc.usage.size()) - { - help += "\n" + pc.usage; - } - lua_pushstring(L, help.c_str()); - return 1; - } + if ((*plugin)[i].name == command) + return &(*plugin)[i]; } // not found (somehow) - lua_pushnil(L); + return NULL; +} + +static int internal_getCommandHelp(lua_State *L) +{ + const PluginCommand *pc = getPluginCommand(luaL_checkstring(L, 1)); + if (!pc) + { + lua_pushnil(L); + return 1; + } + + std::string help = pc->description; + if (help.size() && help[help.size()-1] != '.') + help += "."; + if (pc->usage.size()) + help += "\n" + pc->usage; + lua_pushstring(L, help.c_str()); + return 1; +} + +static int internal_getCommandDescription(lua_State *L) +{ + const PluginCommand *pc = getPluginCommand(luaL_checkstring(L, 1)); + if (!pc) + { + lua_pushnil(L); + return 1; + } + + std::string help = pc->description; + if (help.size() && help[help.size()-1] != '.') + help += "."; + lua_pushstring(L, help.c_str()); return 1; } @@ -3218,6 +3235,7 @@ static const luaL_Reg dfhack_internal_funcs[] = { { "getAddress", internal_getAddress }, { "setAddress", internal_setAddress }, { "getVTable", internal_getVTable }, + { "adjustOffset", internal_adjustOffset }, { "getMemRanges", internal_getMemRanges }, { "patchMemory", internal_patchMemory }, @@ -3236,6 +3254,7 @@ static const luaL_Reg dfhack_internal_funcs[] = { { "listPlugins", internal_listPlugins }, { "listCommands", internal_listCommands }, { "getCommandHelp", internal_getCommandHelp }, + { "getCommandDescription", internal_getCommandDescription }, { "isPluginEnableable", internal_isPluginEnableable }, { "threadid", internal_threadid }, { "md5File", internal_md5file }, diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index e35d2f125..647d08996 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -32,58 +32,75 @@ local ENTRY_TYPES = { } local HELP_SOURCES = { - STUB='stub', - RENDERED='rendered', - PLUGIN='plugin', - SCRIPT='script', + RENDERED='rendered', -- from the installed, rendered help text + PLUGIN='plugin', -- from the plugin source code + SCRIPT='script', -- from the script source code + STUB='stub', -- from a generated stub } --- builtins +-- builtin command names, with aliases mapped to their canonical form local BUILTINS = { - 'alias', - 'clear', - 'cls', - 'devel/dump-rpc', - 'die', - 'dir', - 'disable', - 'enable', - 'fpause', - 'help', - 'hide', - 'keybinding', - 'kill-lua', - 'load', - 'ls', - 'man', - 'plug', - 'reload', - 'script', - 'sc-script', - 'show', - 'tags', - 'type', - 'unload', + alias=true, + clear='cls', + cls=true, + ['devel/dump-rpc']=true, + die=true, + dir='ls', + disable=true, + enable=true, + fpause=true, + help=true, + hide=true, + keybinding=true, + ['kill-lua']=true, + ['load']=true, + ls=true, + man='help', + plug=true, + reload=true, + script=true, + ['sc-script']=true, + show=true, + tags=true, + ['type']=true, + unload=true, } +--------------------------------------------------------------------------- +-- data structures +--------------------------------------------------------------------------- + +-- help text database, keys are a subset of the entry database -- entry name -> { --- entry_types (set of ENTRY_TYPES), +-- help_source (element of HELP_SOURCES), -- short_help (string), -- long_help (string), -- tags (set), --- help_source (element of HELP_SOURCES), -- source_timestamp (mtime, 0 for non-files), -- source_path (string, nil for non-files) -- } +textdb = textdb or {} + +-- entry database, points to text in textdb +-- entry name -> { +-- entry_types (set of ENTRY_TYPES), +-- short_help (string, if not nil then overrides short_help in text_entry), +-- text_entry (string) +-- } -- -- entry_types is a set because plugin commands can also be the plugin names. -db = db or {} +entrydb = entrydb or {} + -- tag name -> list of entry names -- Tags defined in the TAG_DEFINITIONS file that have no associated db entries -- will have an empty list. tag_index = tag_index or {} +--------------------------------------------------------------------------- +-- data ingestion +--------------------------------------------------------------------------- + local function get_rendered_path(entry_name) return RENDERED_PATH .. entry_name .. '.txt' end @@ -98,18 +115,16 @@ local DEFAULT_HELP_TEMPLATE = [[ No help available. ]] -local function make_default_entry(entry_name, entry_types, source, - source_timestamp, source_path) +local function make_default_entry(entry_name, help_source, kwargs) local default_long_help = DEFAULT_HELP_TEMPLATE:format( entry_name, ('*'):rep(#entry_name)) return { - entry_types=entry_types, + help_source=help_source, short_help='No help available.', long_help=default_long_help, tags={}, - help_source=source, - source_timestamp=source_timestamp or 0, - source_path=source_path} + source_timestamp=kwargs.source_timestamp or 0, + source_path=kwargs.source_path} end -- updates the short_text, the long_text, and the tags in the given entry based @@ -129,7 +144,8 @@ local function update_entry(entry, iterator, opts) local lines = {} local first_line_is_short_help = opts.first_line_is_short_help local begin_marker_found,header_found = not opts.begin_marker,opts.no_header - local tags_found, short_help_found, in_short_help = false, false, false + local tags_found, short_help_found = false, opts.skip_short_help + local in_short_help = false for line in iterator do if not short_help_found and first_line_is_short_help then line = line:trim() @@ -193,31 +209,30 @@ local function update_entry(entry, iterator, opts) end -- create db entry based on parsing sphinx-rendered help text -local function make_rendered_entry(old_entry, entry_name, entry_types) - local rendered_path = get_rendered_path(entry_name) - local source_timestamp = dfhack.filesystem.mtime(rendered_path) - if old_entry and old_entry.source == HELP_SOURCES.RENDERED and +local function make_rendered_entry(old_entry, entry_name, kwargs) + local source_path = get_rendered_path(entry_name) + local source_timestamp = dfhack.filesystem.mtime(source_path) + if old_entry and old_entry.help_source == HELP_SOURCES.RENDERED and old_entry.source_timestamp >= source_timestamp then -- we already have the latest info return old_entry end - local entry = make_default_entry(entry_name, entry_types, - HELP_SOURCES.RENDERED, source_timestamp, rendered_path) - update_entry(entry, io.lines(rendered_path)) + kwargs.source_path, kwargs.source_timestamp = source_path, source_timestamp + local entry = make_default_entry(entry_name, HELP_SOURCES.RENDERED, kwargs) + update_entry(entry, io.lines(source_path)) return entry end -- create db entry based on the help text in the plugin source (used by -- out-of-tree plugins) -local function make_plugin_entry(old_entry, entry_name, entry_types) +local function make_plugin_entry(old_entry, entry_name, kwargs) if old_entry and old_entry.source == HELP_SOURCES.PLUGIN then -- we can't tell when a plugin is reloaded, so we can either choose to -- always refresh or never refresh. let's go with never for now for -- performance. return old_entry end - local entry = make_default_entry(entry_name, entry_types, - HELP_SOURCES.PLUGIN) + local entry = make_default_entry(entry_name, HELP_SOURCES.PLUGIN, kwargs) local long_help = dfhack.internal.getCommandHelp(entry_name) if long_help and #long_help:trim() > 0 then update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true}) @@ -227,19 +242,22 @@ end -- create db entry based on the help text in the script source (used by -- out-of-tree scripts) -local function make_script_entry(old_entry, entry_name, script_source_path) - local source_timestamp = dfhack.filesystem.mtime(script_source_path) +local function make_script_entry(old_entry, entry_name, kwargs) + local source_path = kwargs.source_path + local source_timestamp = dfhack.filesystem.mtime(source_path) if old_entry and old_entry.source == HELP_SOURCES.SCRIPT and - old_entry.script_source_path == script_source_path and + old_entry.source_path == source_path and old_entry.source_timestamp >= source_timestamp then -- we already have the latest info return old_entry end - local entry = make_default_entry(entry_name, {[ENTRY_TYPES.COMMAND]=true}, - HELP_SOURCES.SCRIPT, source_timestamp, script_source_path) - local ok, lines = pcall(io.lines, script_source_path) - if not ok then return entry end - local is_rb = script_source_path:endswith('.rb') + kwargs.source_timestamp, kwargs.entry_type = source_timestamp + local entry = make_default_entry(entry_name, HELP_SOURCES.SCRIPT, kwargs) + local ok, lines = pcall(io.lines, source_path) + if not ok then + return entry + end + local is_rb = source_path:endswith('.rb') update_entry(entry, lines, {begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN), end_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_END), @@ -247,78 +265,81 @@ local function make_script_entry(old_entry, entry_name, script_source_path) return entry end --- updates the db (and associated tag index) with a new entry if the entry_name --- doesn't already exist in the db. -local function update_db(old_db, db, source, entry_name, kwargs) - if db[entry_name] then +-- updates the dbs (and associated tag index) with a new entry if the entry_name +-- doesn't already exist in the dbs. +local function update_db(old_db, entry_name, text_entry, help_source, kwargs) + if entrydb[entry_name] then -- already in db (e.g. from a higher-priority script dir); skip return end - local entry, old_entry = nil, old_db[entry_name] - if source == HELP_SOURCES.RENDERED then - entry = make_rendered_entry(old_entry, entry_name, kwargs.entry_types) - elseif source == HELP_SOURCES.PLUGIN then - entry = make_plugin_entry(old_entry, entry_name, kwargs.entry_types) - elseif source == HELP_SOURCES.SCRIPT then - entry = make_script_entry(old_entry, entry_name, kwargs.script_source) - elseif source == HELP_SOURCES.STUB then - entry = make_default_entry(entry_name, kwargs.entry_types, - HELP_SOURCES.STUB) - else - error('unhandled help source: ' .. source) + entrydb[entry_name] = { + entry_types=kwargs.entry_types, + short_help=kwargs.short_help, + text_entry=text_entry + } + if entry_name ~= text_entry then + return end - db[entry_name] = entry - for tag in pairs(entry.tags) do - -- ignore unknown tags - if tag_index[tag] then - table.insert(tag_index[tag], entry_name) - end + + local text_entry, old_entry = nil, old_db[entry_name] + if help_source == HELP_SOURCES.RENDERED then + text_entry = make_rendered_entry(old_entry, entry_name, kwargs) + elseif help_source == HELP_SOURCES.PLUGIN then + text_entry = make_plugin_entry(old_entry, entry_name, kwargs) + elseif help_source == HELP_SOURCES.SCRIPT then + text_entry = make_script_entry(old_entry, entry_name, kwargs) + elseif help_source == HELP_SOURCES.STUB then + text_entry = make_default_entry(entry_name, HELP_SOURCES.STUB, kwargs) + else + error('unhandled help source: ' .. help_source) end + textdb[entry_name] = text_entry end -- add the builtin commands to the db -local function scan_builtins(old_db, db) +local function scan_builtins(old_db) local entry_types = {[ENTRY_TYPES.BUILTIN]=true, [ENTRY_TYPES.COMMAND]=true} - for _,builtin in ipairs(BUILTINS) do - update_db(old_db, db, - has_rendered_help(builtin) and - HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, - builtin, - {entry_types=entry_types}) + for builtin,canonical in pairs(BUILTINS) do + if canonical == true then canonical = builtin end + update_db(old_db, builtin, canonical, + has_rendered_help(canonical) and + HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, + {entry_types=entry_types}) end end --- scan for plugins and plugin-provided commands and add their help to the db -local function scan_plugins(old_db, db) +-- scan for enableable plugins and plugin-provided commands and add their help +-- to the db +local function scan_plugins(old_db) local plugin_names = dfhack.internal.listPlugins() for _,plugin in ipairs(plugin_names) do local commands = dfhack.internal.listCommands(plugin) - local includes_plugin = false + local includes_plugin, has_commands = false, false for _,command in ipairs(commands) do - local entry_types = {[ENTRY_TYPES.COMMAND]=true} + local kwargs = {entry_types={[ENTRY_TYPES.COMMAND]=true}} if command == plugin then - entry_types[ENTRY_TYPES.PLUGIN]=true + kwargs.entry_types[ENTRY_TYPES.PLUGIN]=true includes_plugin = true end - update_db(old_db, db, - has_rendered_help(command) and + kwargs.short_help = dfhack.internal.getCommandDescription(command) + update_db(old_db, command, plugin, + has_rendered_help(plugin) and HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN, - command, {entry_types=entry_types}) + kwargs) + has_commands = true end - if not includes_plugin and - dfhack.internal.isPluginEnableable(plugin) then - update_db(old_db, db, + if not includes_plugin and (has_commands or + dfhack.internal.isPluginEnableable(plugin)) then + update_db(old_db, plugin, plugin, has_rendered_help(plugin) and HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, - plugin, {entry_types={[ENTRY_TYPES.PLUGIN]=true}}) - goto continue + {entry_types={[ENTRY_TYPES.PLUGIN]=true}}) end - ::continue:: end end -- scan for scripts and add their help to the db -local function scan_scripts(old_db, db) +local function scan_scripts(old_db) local entry_types = {[ENTRY_TYPES.COMMAND]=true} for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do local files = dfhack.filesystem.listdir_recursive( @@ -333,12 +354,11 @@ local function scan_scripts(old_db, db) end local dot_index = f.path:find('%.[^.]*$') local entry_name = f.path:sub(1, dot_index - 1) - local script_source = script_path .. '/' .. f.path - update_db(old_db, db, + local source_path = script_path .. '/' .. f.path + update_db(old_db, entry_name, entry_name, has_rendered_help(entry_name) and HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT, - entry_name, - {entry_types=entry_types, script_source=script_source}) + {entry_types=entry_types, source_path=source_path}) ::continue:: end ::skip_path:: @@ -370,6 +390,17 @@ local function initialize_tags() end end +local function index_tags() + for entry_name,entry in pairs(entrydb) do + for tag in pairs(textdb[entry.text_entry].tags) do + -- ignore unknown tags + if tag_index[tag] then + table.insert(tag_index[tag], entry_name) + end + end + end +end + -- ensures the db is up to date by scanning all help sources. does not do -- anything if it has already been run within the last 60 seconds. last_refresh_ms = last_refresh_ms or 0 @@ -378,13 +409,14 @@ local function ensure_db() if now_ms - last_refresh_ms < 60000 then return end last_refresh_ms = now_ms - local old_db = db - db, tag_index = {}, {} + local old_db = textdb + textdb, entrydb, tag_index = {}, {}, {} initialize_tags() - scan_builtins(old_db, db) - scan_plugins(old_db, db) - scan_scripts(old_db, db) + scan_builtins(old_db) + scan_plugins(old_db) + scan_scripts(old_db) + index_tags() end --------------------------------------------------------------------------- @@ -415,15 +447,16 @@ end -- returns whether the given string (or list of strings) is an entry in the db function is_entry(str) - return has_keys(str, db) + return has_keys(str, entrydb) end local function get_db_property(entry_name, property) ensure_db() - if not db[entry_name] then + if not entrydb[entry_name] then error(('helpdb entry not found: "%s"'):format(entry_name)) end - return db[entry_name][property] + return entrydb[entry_name][property] or + textdb[entrydb[entry_name].text_entry][property] end -- returns the ~54 char summary blurb associated with the entry @@ -504,11 +537,11 @@ local function sort_by_basename(a, b) end local function matches(entry_name, filter) - local db_entry = db[entry_name] if filter.tag then local matched = false + local tags = get_db_property(entry_name, 'tags') for _,tag in ipairs(filter.tag) do - if db_entry.tags[tag] then + if tags[tag] then matched = true break end @@ -519,8 +552,9 @@ local function matches(entry_name, filter) end if filter.types then local matched = false + local etypes = get_db_property(entry_name, 'entry_types') for _,etype in ipairs(filter.types) do - if db_entry.entry_types[etype] then + if etypes[etype] then matched = true break end @@ -577,7 +611,7 @@ function search_entries(include, exclude) include = normalize_filter(include) exclude = normalize_filter(exclude) local entries = {} - for entry in pairs(db) do + for entry in pairs(entrydb) do if (not include or matches(entry, include)) and (not exclude or not matches(entry, exclude)) then table.insert(entries, entry) @@ -595,7 +629,8 @@ end function is_builtin(command) ensure_db() - return db[command] and db[command].entry_types[ENTRY_TYPES.BUILTIN] + return entrydb[command] and + get_db_property(entry_name, 'entry_types')[ENTRY_TYPES.BUILTIN] end --------------------------------------------------------------------------- @@ -605,7 +640,7 @@ end -- implements the 'help' builtin command function help(entry) ensure_db() - if not db[entry] then + if not entrydb[entry] then dfhack.printerr(('No help entry found for "%s"'):format(entry)) return end