read plugin command docs from single plugin file

develop
myk002 2022-07-21 22:33:43 -07:00
parent 5521a5a45d
commit 2ce7518562
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
2 changed files with 196 additions and 142 deletions

@ -3109,41 +3109,58 @@ static int internal_listCommands(lua_State *L)
} }
return 1; return 1;
} }
static int internal_getCommandHelp(lua_State *L)
static const PluginCommand * getPluginCommand(const char * command)
{ {
auto plugins = Core::getInstance().getPluginManager(); auto plugins = Core::getInstance().getPluginManager();
auto plugin = plugins->getPluginByCommand(command);
const char *name = luaL_checkstring(L, 1);
auto plugin = plugins->getPluginByCommand(name);
if (!plugin) if (!plugin)
{ {
lua_pushnil(L); return NULL;
return 1;
} }
size_t num_commands = plugin->size(); size_t num_commands = plugin->size();
for (size_t i = 0; i < num_commands; ++i) for (size_t i = 0; i < num_commands; ++i)
{ {
if ((*plugin)[i].name == name) if ((*plugin)[i].name == command)
{ return &(*plugin)[i];
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;
}
} }
// not found (somehow) // 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; return 1;
} }
@ -3218,6 +3235,7 @@ static const luaL_Reg dfhack_internal_funcs[] = {
{ "getAddress", internal_getAddress }, { "getAddress", internal_getAddress },
{ "setAddress", internal_setAddress }, { "setAddress", internal_setAddress },
{ "getVTable", internal_getVTable }, { "getVTable", internal_getVTable },
{ "adjustOffset", internal_adjustOffset }, { "adjustOffset", internal_adjustOffset },
{ "getMemRanges", internal_getMemRanges }, { "getMemRanges", internal_getMemRanges },
{ "patchMemory", internal_patchMemory }, { "patchMemory", internal_patchMemory },
@ -3236,6 +3254,7 @@ static const luaL_Reg dfhack_internal_funcs[] = {
{ "listPlugins", internal_listPlugins }, { "listPlugins", internal_listPlugins },
{ "listCommands", internal_listCommands }, { "listCommands", internal_listCommands },
{ "getCommandHelp", internal_getCommandHelp }, { "getCommandHelp", internal_getCommandHelp },
{ "getCommandDescription", internal_getCommandDescription },
{ "isPluginEnableable", internal_isPluginEnableable }, { "isPluginEnableable", internal_isPluginEnableable },
{ "threadid", internal_threadid }, { "threadid", internal_threadid },
{ "md5File", internal_md5file }, { "md5File", internal_md5file },

@ -32,58 +32,75 @@ local ENTRY_TYPES = {
} }
local HELP_SOURCES = { local HELP_SOURCES = {
STUB='stub', RENDERED='rendered', -- from the installed, rendered help text
RENDERED='rendered', PLUGIN='plugin', -- from the plugin source code
PLUGIN='plugin', SCRIPT='script', -- from the script source code
SCRIPT='script', STUB='stub', -- from a generated stub
} }
-- builtins -- builtin command names, with aliases mapped to their canonical form
local BUILTINS = { local BUILTINS = {
'alias', alias=true,
'clear', clear='cls',
'cls', cls=true,
'devel/dump-rpc', ['devel/dump-rpc']=true,
'die', die=true,
'dir', dir='ls',
'disable', disable=true,
'enable', enable=true,
'fpause', fpause=true,
'help', help=true,
'hide', hide=true,
'keybinding', keybinding=true,
'kill-lua', ['kill-lua']=true,
'load', ['load']=true,
'ls', ls=true,
'man', man='help',
'plug', plug=true,
'reload', reload=true,
'script', script=true,
'sc-script', ['sc-script']=true,
'show', show=true,
'tags', tags=true,
'type', ['type']=true,
'unload', unload=true,
} }
---------------------------------------------------------------------------
-- data structures
---------------------------------------------------------------------------
-- help text database, keys are a subset of the entry database
-- entry name -> { -- entry name -> {
-- entry_types (set of ENTRY_TYPES), -- help_source (element of HELP_SOURCES),
-- short_help (string), -- short_help (string),
-- long_help (string), -- long_help (string),
-- tags (set), -- tags (set),
-- help_source (element of HELP_SOURCES),
-- source_timestamp (mtime, 0 for non-files), -- source_timestamp (mtime, 0 for non-files),
-- source_path (string, nil 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. -- 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 -- tag name -> list of entry names
-- Tags defined in the TAG_DEFINITIONS file that have no associated db entries -- Tags defined in the TAG_DEFINITIONS file that have no associated db entries
-- will have an empty list. -- will have an empty list.
tag_index = tag_index or {} tag_index = tag_index or {}
---------------------------------------------------------------------------
-- data ingestion
---------------------------------------------------------------------------
local function get_rendered_path(entry_name) local function get_rendered_path(entry_name)
return RENDERED_PATH .. entry_name .. '.txt' return RENDERED_PATH .. entry_name .. '.txt'
end end
@ -98,18 +115,16 @@ local DEFAULT_HELP_TEMPLATE = [[
No help available. No help available.
]] ]]
local function make_default_entry(entry_name, entry_types, source, local function make_default_entry(entry_name, help_source, kwargs)
source_timestamp, source_path)
local default_long_help = DEFAULT_HELP_TEMPLATE:format( local default_long_help = DEFAULT_HELP_TEMPLATE:format(
entry_name, ('*'):rep(#entry_name)) entry_name, ('*'):rep(#entry_name))
return { return {
entry_types=entry_types, help_source=help_source,
short_help='No help available.', short_help='No help available.',
long_help=default_long_help, long_help=default_long_help,
tags={}, tags={},
help_source=source, source_timestamp=kwargs.source_timestamp or 0,
source_timestamp=source_timestamp or 0, source_path=kwargs.source_path}
source_path=source_path}
end end
-- updates the short_text, the long_text, and the tags in the given entry based -- 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 lines = {}
local first_line_is_short_help = opts.first_line_is_short_help 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 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 for line in iterator do
if not short_help_found and first_line_is_short_help then if not short_help_found and first_line_is_short_help then
line = line:trim() line = line:trim()
@ -193,31 +209,30 @@ local function update_entry(entry, iterator, opts)
end end
-- create db entry based on parsing sphinx-rendered help text -- create db entry based on parsing sphinx-rendered help text
local function make_rendered_entry(old_entry, entry_name, entry_types) local function make_rendered_entry(old_entry, entry_name, kwargs)
local rendered_path = get_rendered_path(entry_name) local source_path = get_rendered_path(entry_name)
local source_timestamp = dfhack.filesystem.mtime(rendered_path) local source_timestamp = dfhack.filesystem.mtime(source_path)
if old_entry and old_entry.source == HELP_SOURCES.RENDERED and if old_entry and old_entry.help_source == HELP_SOURCES.RENDERED and
old_entry.source_timestamp >= source_timestamp then old_entry.source_timestamp >= source_timestamp then
-- we already have the latest info -- we already have the latest info
return old_entry return old_entry
end end
local entry = make_default_entry(entry_name, entry_types, kwargs.source_path, kwargs.source_timestamp = source_path, source_timestamp
HELP_SOURCES.RENDERED, source_timestamp, rendered_path) local entry = make_default_entry(entry_name, HELP_SOURCES.RENDERED, kwargs)
update_entry(entry, io.lines(rendered_path)) update_entry(entry, io.lines(source_path))
return entry return entry
end end
-- create db entry based on the help text in the plugin source (used by -- create db entry based on the help text in the plugin source (used by
-- out-of-tree plugins) -- 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 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 -- 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 -- always refresh or never refresh. let's go with never for now for
-- performance. -- performance.
return old_entry return old_entry
end end
local entry = make_default_entry(entry_name, entry_types, local entry = make_default_entry(entry_name, HELP_SOURCES.PLUGIN, kwargs)
HELP_SOURCES.PLUGIN)
local long_help = dfhack.internal.getCommandHelp(entry_name) local long_help = dfhack.internal.getCommandHelp(entry_name)
if long_help and #long_help:trim() > 0 then if long_help and #long_help:trim() > 0 then
update_entry(entry, long_help:trim():gmatch('[^\n]*'), {no_header=true}) 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 -- create db entry based on the help text in the script source (used by
-- out-of-tree scripts) -- out-of-tree scripts)
local function make_script_entry(old_entry, entry_name, script_source_path) local function make_script_entry(old_entry, entry_name, kwargs)
local source_timestamp = dfhack.filesystem.mtime(script_source_path) 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 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 old_entry.source_timestamp >= source_timestamp then
-- we already have the latest info -- we already have the latest info
return old_entry return old_entry
end end
local entry = make_default_entry(entry_name, {[ENTRY_TYPES.COMMAND]=true}, kwargs.source_timestamp, kwargs.entry_type = source_timestamp
HELP_SOURCES.SCRIPT, source_timestamp, script_source_path) local entry = make_default_entry(entry_name, HELP_SOURCES.SCRIPT, kwargs)
local ok, lines = pcall(io.lines, script_source_path) local ok, lines = pcall(io.lines, source_path)
if not ok then return entry end if not ok then
local is_rb = script_source_path:endswith('.rb') return entry
end
local is_rb = source_path:endswith('.rb')
update_entry(entry, lines, update_entry(entry, lines,
{begin_marker=(is_rb and SCRIPT_DOC_BEGIN_RUBY or SCRIPT_DOC_BEGIN), {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), 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 return entry
end end
-- updates the db (and associated tag index) with a new entry if the entry_name -- updates the dbs (and associated tag index) with a new entry if the entry_name
-- doesn't already exist in the db. -- doesn't already exist in the dbs.
local function update_db(old_db, db, source, entry_name, kwargs) local function update_db(old_db, entry_name, text_entry, help_source, kwargs)
if db[entry_name] then if entrydb[entry_name] then
-- already in db (e.g. from a higher-priority script dir); skip -- already in db (e.g. from a higher-priority script dir); skip
return return
end end
local entry, old_entry = nil, old_db[entry_name] entrydb[entry_name] = {
if source == HELP_SOURCES.RENDERED then entry_types=kwargs.entry_types,
entry = make_rendered_entry(old_entry, entry_name, kwargs.entry_types) short_help=kwargs.short_help,
elseif source == HELP_SOURCES.PLUGIN then text_entry=text_entry
entry = make_plugin_entry(old_entry, entry_name, kwargs.entry_types) }
elseif source == HELP_SOURCES.SCRIPT then if entry_name ~= text_entry then
entry = make_script_entry(old_entry, entry_name, kwargs.script_source) return
elseif source == HELP_SOURCES.STUB then
entry = make_default_entry(entry_name, kwargs.entry_types,
HELP_SOURCES.STUB)
else
error('unhandled help source: ' .. source)
end end
db[entry_name] = entry
for tag in pairs(entry.tags) do local text_entry, old_entry = nil, old_db[entry_name]
-- ignore unknown tags if help_source == HELP_SOURCES.RENDERED then
if tag_index[tag] then text_entry = make_rendered_entry(old_entry, entry_name, kwargs)
table.insert(tag_index[tag], entry_name) elseif help_source == HELP_SOURCES.PLUGIN then
end 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 end
textdb[entry_name] = text_entry
end end
-- add the builtin commands to the db -- 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} local entry_types = {[ENTRY_TYPES.BUILTIN]=true, [ENTRY_TYPES.COMMAND]=true}
for _,builtin in ipairs(BUILTINS) do for builtin,canonical in pairs(BUILTINS) do
update_db(old_db, db, if canonical == true then canonical = builtin end
has_rendered_help(builtin) and update_db(old_db, builtin, canonical,
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, has_rendered_help(canonical) and
builtin, HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
{entry_types=entry_types}) {entry_types=entry_types})
end end
end end
-- scan for plugins and plugin-provided commands and add their help to the db -- scan for enableable plugins and plugin-provided commands and add their help
local function scan_plugins(old_db, db) -- to the db
local function scan_plugins(old_db)
local plugin_names = dfhack.internal.listPlugins() local plugin_names = dfhack.internal.listPlugins()
for _,plugin in ipairs(plugin_names) do for _,plugin in ipairs(plugin_names) do
local commands = dfhack.internal.listCommands(plugin) local commands = dfhack.internal.listCommands(plugin)
local includes_plugin = false local includes_plugin, has_commands = false, false
for _,command in ipairs(commands) do 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 if command == plugin then
entry_types[ENTRY_TYPES.PLUGIN]=true kwargs.entry_types[ENTRY_TYPES.PLUGIN]=true
includes_plugin = true includes_plugin = true
end end
update_db(old_db, db, kwargs.short_help = dfhack.internal.getCommandDescription(command)
has_rendered_help(command) and update_db(old_db, command, plugin,
has_rendered_help(plugin) and
HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN, HELP_SOURCES.RENDERED or HELP_SOURCES.PLUGIN,
command, {entry_types=entry_types}) kwargs)
has_commands = true
end end
if not includes_plugin and if not includes_plugin and (has_commands or
dfhack.internal.isPluginEnableable(plugin) then dfhack.internal.isPluginEnableable(plugin)) then
update_db(old_db, db, update_db(old_db, plugin, plugin,
has_rendered_help(plugin) and has_rendered_help(plugin) and
HELP_SOURCES.RENDERED or HELP_SOURCES.STUB, HELP_SOURCES.RENDERED or HELP_SOURCES.STUB,
plugin, {entry_types={[ENTRY_TYPES.PLUGIN]=true}}) {entry_types={[ENTRY_TYPES.PLUGIN]=true}})
goto continue
end end
::continue::
end end
end end
-- scan for scripts and add their help to the db -- 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} local entry_types = {[ENTRY_TYPES.COMMAND]=true}
for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do for _,script_path in ipairs(dfhack.internal.getScriptPaths()) do
local files = dfhack.filesystem.listdir_recursive( local files = dfhack.filesystem.listdir_recursive(
@ -333,12 +354,11 @@ local function scan_scripts(old_db, db)
end end
local dot_index = f.path:find('%.[^.]*$') local dot_index = f.path:find('%.[^.]*$')
local entry_name = f.path:sub(1, dot_index - 1) local entry_name = f.path:sub(1, dot_index - 1)
local script_source = script_path .. '/' .. f.path local source_path = script_path .. '/' .. f.path
update_db(old_db, db, update_db(old_db, entry_name, entry_name,
has_rendered_help(entry_name) and has_rendered_help(entry_name) and
HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT, HELP_SOURCES.RENDERED or HELP_SOURCES.SCRIPT,
entry_name, {entry_types=entry_types, source_path=source_path})
{entry_types=entry_types, script_source=script_source})
::continue:: ::continue::
end end
::skip_path:: ::skip_path::
@ -370,6 +390,17 @@ local function initialize_tags()
end end
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 -- 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. -- anything if it has already been run within the last 60 seconds.
last_refresh_ms = last_refresh_ms or 0 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 if now_ms - last_refresh_ms < 60000 then return end
last_refresh_ms = now_ms last_refresh_ms = now_ms
local old_db = db local old_db = textdb
db, tag_index = {}, {} textdb, entrydb, tag_index = {}, {}, {}
initialize_tags() initialize_tags()
scan_builtins(old_db, db) scan_builtins(old_db)
scan_plugins(old_db, db) scan_plugins(old_db)
scan_scripts(old_db, db) scan_scripts(old_db)
index_tags()
end end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
@ -415,15 +447,16 @@ end
-- returns whether the given string (or list of strings) is an entry in the db -- returns whether the given string (or list of strings) is an entry in the db
function is_entry(str) function is_entry(str)
return has_keys(str, db) return has_keys(str, entrydb)
end end
local function get_db_property(entry_name, property) local function get_db_property(entry_name, property)
ensure_db() ensure_db()
if not db[entry_name] then if not entrydb[entry_name] then
error(('helpdb entry not found: "%s"'):format(entry_name)) error(('helpdb entry not found: "%s"'):format(entry_name))
end end
return db[entry_name][property] return entrydb[entry_name][property] or
textdb[entrydb[entry_name].text_entry][property]
end end
-- returns the ~54 char summary blurb associated with the entry -- returns the ~54 char summary blurb associated with the entry
@ -504,11 +537,11 @@ local function sort_by_basename(a, b)
end end
local function matches(entry_name, filter) local function matches(entry_name, filter)
local db_entry = db[entry_name]
if filter.tag then if filter.tag then
local matched = false local matched = false
local tags = get_db_property(entry_name, 'tags')
for _,tag in ipairs(filter.tag) do for _,tag in ipairs(filter.tag) do
if db_entry.tags[tag] then if tags[tag] then
matched = true matched = true
break break
end end
@ -519,8 +552,9 @@ local function matches(entry_name, filter)
end end
if filter.types then if filter.types then
local matched = false local matched = false
local etypes = get_db_property(entry_name, 'entry_types')
for _,etype in ipairs(filter.types) do for _,etype in ipairs(filter.types) do
if db_entry.entry_types[etype] then if etypes[etype] then
matched = true matched = true
break break
end end
@ -577,7 +611,7 @@ function search_entries(include, exclude)
include = normalize_filter(include) include = normalize_filter(include)
exclude = normalize_filter(exclude) exclude = normalize_filter(exclude)
local entries = {} local entries = {}
for entry in pairs(db) do for entry in pairs(entrydb) do
if (not include or matches(entry, include)) and if (not include or matches(entry, include)) and
(not exclude or not matches(entry, exclude)) then (not exclude or not matches(entry, exclude)) then
table.insert(entries, entry) table.insert(entries, entry)
@ -595,7 +629,8 @@ end
function is_builtin(command) function is_builtin(command)
ensure_db() 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 end
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
@ -605,7 +640,7 @@ end
-- implements the 'help' builtin command -- implements the 'help' builtin command
function help(entry) function help(entry)
ensure_db() ensure_db()
if not db[entry] then if not entrydb[entry] then
dfhack.printerr(('No help entry found for "%s"'):format(entry)) dfhack.printerr(('No help entry found for "%s"'):format(entry))
return return
end end