From bec40660330764c59731fbb736af58ffc445bd8c Mon Sep 17 00:00:00 2001 From: "Justin M. Keyes" Date: Sun, 3 Mar 2019 15:01:16 +0100 Subject: gen_vimdoc.py: render nested lists, etc [ci skip] - render_node() is now the main rendering function: it traverses a node and builds the Vim help text recursively. - render_para() is weird and ugly, it is the entry-point for rendering the help text for one docstring'd function. --- runtime/doc/api.txt | 511 ++++++++++++++++++---------------- scripts/gen_api_vimdoc.py | 563 ------------------------------------- scripts/gen_vimdoc.py | 690 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 960 insertions(+), 804 deletions(-) delete mode 100755 scripts/gen_api_vimdoc.py create mode 100755 scripts/gen_vimdoc.py diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index bfa0618b51..52994ce073 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -293,6 +293,9 @@ nvim_get_hl_by_name({name}, {rgb}) *nvim_get_hl_by_name()* Return: ~ Highlight definition map + See also: ~ + nvim_get_hl_by_id + nvim_get_hl_by_id({hl_id}, {rgb}) *nvim_get_hl_by_id()* Gets a highlight definition by id. |hlID()| @@ -303,6 +306,9 @@ nvim_get_hl_by_id({hl_id}, {rgb}) *nvim_get_hl_by_id()* Return: ~ Highlight definition map + See also: ~ + nvim_get_hl_by_name + nvim_feedkeys({keys}, {mode}, {escape_csi}) *nvim_feedkeys()* Sends input-keys to Nvim, subject to various quirks controlled by `mode` flags. This is a blocking call, unlike @@ -316,6 +322,10 @@ nvim_feedkeys({keys}, {mode}, {escape_csi}) *nvim_feedkeys()* {escape_csi} If true, escape K_SPECIAL/CSI bytes in `keys` + See also: ~ + feedkeys() + vim_strsave_escape_csi + nvim_input({keys}) *nvim_input()* Queues raw user-input. Unlike |nvim_feedkeys()|, this uses a low-level input buffer and the call is non-blocking (input is @@ -326,7 +336,6 @@ nvim_input({keys}) *nvim_input()* Note: |keycodes| like are translated, so "<" is special. To input a literal "<", send . - Note: For mouse events use |nvim_input_mouse()|. The pseudokey form "" is deprecated since @@ -346,9 +355,8 @@ nvim_input({keys}) *nvim_input()* nvim_input_mouse({button}, {action}, {modifier}, {grid}, {row}, {col}) Send mouse event from GUI. - The call is non-blocking. It doesn't wait on any resulting - action, but queues the event to be processed soon by the event - loop. + Non-blocking: does not wait on any result, but queues the + event to be processed soon by the event loop. Note: Currently this doesn't support "scripting" multiple mouse @@ -392,6 +400,10 @@ nvim_replace_termcodes({str}, {from_part}, {do_lt}, {special}) {special} Replace |keycodes|, e.g. becomes a "\n" char. + See also: ~ + replace_termcodes + cpoptions + nvim_command_output({command}) *nvim_command_output()* Executes an ex-command and returns its (non-error) output. Shell |:!| output is not captured. @@ -420,7 +432,7 @@ nvim_execute_lua({code}, {args}) *nvim_execute_lua()* inside the chunk. The chunk can return a value. Only statements are executed. To evaluate an expression, - prefix it with `return`: return my_function(...) + prefix it with `return` : return my_function(...) Parameters: ~ {code} lua code to execute @@ -458,7 +470,7 @@ nvim_call_dict_function({dict}, {fn}, {args}) *nvim_call_dict_function()* Result of the function call nvim_strwidth({text}) *nvim_strwidth()* - Calculates the number of display cells occupied by `text`. + Calculates the number of display cells occupied by `text` . counts as one cell. Parameters: ~ @@ -575,11 +587,14 @@ nvim_err_writeln({str}) *nvim_err_writeln()* Parameters: ~ {str} Message + See also: ~ + nvim_err_write() + nvim_list_bufs() *nvim_list_bufs()* Gets the current list of buffer handles - Includes unlisted (unloaded/deleted) buffers, like `:ls!`. Use - |nvim_buf_is_loaded()| to check if a buffer is loaded. + Includes unlisted (unloaded/deleted) buffers, like `:ls!` . + Use |nvim_buf_is_loaded()| to check if a buffer is loaded. Return: ~ List of buffer handles @@ -618,13 +633,16 @@ nvim_create_buf({listed}, {scratch}) *nvim_create_buf()* Creates a new, empty, unnamed buffer. Parameters: ~ - {listed} Controls 'buflisted' + {listed} Sets 'buflisted' {scratch} Creates a "throwaway" |scratch-buffer| for temporary work (always 'nomodified') Return: ~ Buffer handle, or 0 on error + See also: ~ + buf_open_scratch + nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* Open a new window. @@ -639,51 +657,6 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* Exactly one of `external` and `relative` must be specified. - Parameters: ~ - {buffer} handle of buffer to be displayed in the window - {enter} whether the window should be entered (made the - current window) - {config} Dictionary for the window configuration accepts - these keys: - - `relative`: If set, the window becomes a - floating window. The window will be placed with - row,col coordinates relative to one of the - following: - "editor" the global editor grid - "win" a window. Use `win` to specify window id, - or current window will be used by default. - "cursor" the cursor position in current window. - - `win`: when using `relative='win'`, window id of the window - where the position is defined. - - `anchor`: the corner of the float that the row,col - position defines - "NW" north-west (default) - "NE" north-east - "SW" south-west - "SE" south-east - - `focusable`: Whether window can be focused by wincmds and - mouse events. Defaults to true. Even if set to false, - the window can still be entered using - |nvim_set_current_win()| API call. - - `height`: Window height in character cells. Minimum of 1. - - `width`: Window width in character cells. Minimum of 2. - - `row`: row position. Screen cell height are used as unit. - Can be floating point. - - `col`: column position. Screen cell width is used as - unit. Can be floating point. - - `external`: GUI should display the window as an external - top-level window. Currently accepts no other - positioning configuration together with this. - With editor positioning row=0, col=0 refers to the top-left corner of the screen-grid and row=Lines-1, Columns-1 refers to the bottom-right corner. Floating point values are allowed, @@ -698,9 +671,49 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* should not be used to specify arbitrary WM screen positions. Parameters: ~ - - Return: ~ - the window handle or 0 when error + {buffer} handle of buffer to be displayed in the window + {enter} whether the window should be entered (made the + current window) + {config} Dictionary for the window configuration accepts + these keys: + - `relative` : If set, the window becomes a floating + window. The window will be placed with row,col + coordinates relative to one of the following: + - "editor" the global editor grid + - "win" a window. Use `win` to specify a + window id, or the current window will be + used by default. "cursor" the cursor + position in current window. + + - `win` : When using relative='win', window id + of the window where the position is defined. + - `anchor` : The corner of the float that the row,col + position defines: + - "NW" north-west (default) + - "NE" north-east + - "SW" south-west + - "SE" south-east + + - `height` : window height (in character cells). + Minimum of 1. + - `width` : window width (in character cells). + Minimum of 2. + - `row` : row position. Screen cell height are + used as unit. Can be floating point. + - `col` : column position. Screen cell width is + used as unit. Can be floating point. + - `focusable` : Whether window can be focused by + wincmds and mouse events. Defaults to true. + Even if set to false, the window can still be + entered using |nvim_set_current_win()| API + call. + - `external` : GUI should display the window as + an external top-level window. Currently + accepts no other positioning configuration + together with this. + + Return: ~ + Window handle, or 0 on error nvim_list_tabpages() *nvim_list_tabpages()* Gets the current list of tabpage handles. @@ -808,86 +821,87 @@ nvim_get_api_info() *nvim_get_api_info()* *nvim_set_client_info()* nvim_set_client_info({name}, {version}, {type}, {methods}, {attributes}) - Identify the client for nvim. Can be called more than once, - but subsequent calls will remove earlier info, which should be - resent if it is still valid. (This could happen if a library - first identifies the channel, and a plugin using that library - later overrides that info) - - Parameters: ~ - {name} short name for the connected client - {version} Dictionary describing the version, with the - following possible keys (all optional) - "major" major version (defaults to 0 if not - set, for no release yet) "minor" minor - version "patch" patch number "prerelease" - string describing a prerelease, like "dev" - or "beta1" "commit" hash or similar - identifier of commit - {type} Must be one of the following values. A - client library should use "remote" if the - library user hasn't specified other value. - "remote" remote client that connected to - nvim. "ui" gui frontend "embedder" - application using nvim as a component, for - instance IDE/editor implementing a vim mode. - "host" plugin host, typically started by - nvim "plugin" single plugin, started by - nvim + Identifies the client. Can be called more than once; + subsequent calls remove earlier info, which should be included + by the caller if it is still valid. (E.g. if a library first + identifies the channel, then a plugin using that library later + overrides that info) + + Parameters: ~ + {name} Short name for the connected client + {version} Dictionary describing the version, with + these (optional) keys: + - "major" major version (defaults to 0 if + not set, for no release yet) + - "minor" minor version + - "patch" patch number + - "prerelease" string describing a + prerelease, like "dev" or "beta1" + - "commit" hash or similar identifier of + commit + {type} Must be one of the following values. Client + libraries should default to "remote" unless + overridden by the user. + - "remote" remote client connected to Nvim. + - "ui" gui frontend + - "embedder" application using Nvim as a + component (for example, IDE/editor + implementing a vim mode). + - "host" plugin host, typically started by + nvim + - "plugin" single plugin, started by nvim {methods} Builtin methods in the client. For a host, this does not include plugin methods which will be discovered later. The key should be the method name, the values are dicts with - the following (optional) keys: "async" if - true, send as a notification. If false or - unspecified, use a blocking request "nargs" - Number of arguments. Could be a single - integer or an array two integers, minimum - and maximum inclusive. Further keys might be - added in later versions of nvim and unknown - keys are thus ignored. Clients must only use - keys defined in this or later versions of - nvim! - {attributes} Informal attributes describing the client. - Clients might define their own keys, but the - following are suggested: "website" Website - of client (for instance github repository) - "license" Informal description of the - license, such as "Apache 2", "GPLv3" or - "MIT" "logo" URI or path to image, - preferably small logo or icon. .png or .svg - format is preferred. + these (optional) keys (more keys may be + added in future versions of Nvim, thus + unknown keys are ignored. Clients must only + use keys defined in this or later versions + of Nvim): + - "async" if true, send as a notification. + If false or unspecified, use a blocking + request + - "nargs" Number of arguments. Could be a + single integer or an array of two + integers, minimum and maximum inclusive. + {attributes} Arbitrary string:string map of informal + client properties. Suggested keys: + - "website": Client homepage URL (e.g. + GitHub repository) + - "license": License description ("Apache + 2", "GPLv3", "MIT", …) + - "logo": URI or path to image, preferably + small logo or icon. .png or .svg format is + preferred. nvim_get_chan_info({chan}) *nvim_get_chan_info()* Get information about a channel. Return: ~ - a Dictionary, describing a channel with the following - keys: - - `stream`: the stream underlying the channel: - "stdio" stdin and stdout of this Nvim instance. - "stderr" stderr of this Nvim instance. - "socket" TCP/IP socket or named pipe. - "job" job with communication over its stdio. - - `mode`: how data received on the channel is interpreted: - "bytes" send and recieve raw bytes. - "terminal" a |terminal| instance interprets ASCII sequences - "rpc" |RPC| communication on the channel is active - - `pty`: Name of pseudoterminal, if one is used (optional). - On a POSIX system, this will be a device path like - /dev/pts/1. Even if the name is unknown, the key will - still be present to indicate a pty is used. This is - currently the case when using winpty on windows. - - `buffer`: buffer with connected |terminal| - instance (optional). - - `client`: information about the client on the other end of - the RPC channel, if it has added it using - |nvim_set_client_info()| (optional). + Dictionary describing a channel, with these keys: + - "stream" the stream underlying the channel + - "stdio" stdin and stdout of this Nvim instance + - "stderr" stderr of this Nvim instance + - "socket" TCP/IP socket or named pipe + - "job" job with communication over its stdio + + - "mode" how data received on the channel is interpreted + - "bytes" send and receive raw bytes + - "terminal" a |terminal| instance interprets ASCII + sequences + - "rpc" |RPC| communication on the channel is active + + - "pty" Name of pseudoterminal, if one is used (optional). + On a POSIX system, this will be a device path like + /dev/pts/1. Even if the name is unknown, the key will + still be present to indicate a pty is used. This is + currently the case when using winpty on windows. + - "buffer" buffer with connected |terminal| instance + (optional) + - "client" information about the client on the other end + of the RPC channel, if it has added it using + |nvim_set_client_info()|. (optional) nvim_list_chans() *nvim_list_chans()* Get information about all open channels. @@ -901,12 +915,12 @@ nvim_call_atomic({calls}) *nvim_call_atomic()* This has two main usages: 1. To perform several requests from an async context - atomically, i.e. without interleaving redraws, RPC requests - from other clients, or user interactions (however API methods - may trigger autocommands or event processing which have such - side-effects, e.g. |:sleep| may wake timers). + atomically, i.e. without interleaving redraws, RPC requests + from other clients, or user interactions (however API + methods may trigger autocommands or event processing which + have such side-effects, e.g. |:sleep| may wake timers). 2. To minimize RPC overhead (roundtrips) of a sequence of many - requests. + requests. Parameters: ~ {calls} an array of calls, where each call is described @@ -914,13 +928,13 @@ nvim_call_atomic({calls}) *nvim_call_atomic()* and an array of arguments. Return: ~ - an array with two elements. The first is an array of - return values. The second is NIL if all calls succeeded. - If a call resulted in an error, it is a three-element - array with the zero-based index of the call which resulted - in an error, the error type and the error message. If an - error occurred, the values from all preceding calls will - still be returned. + Array of two elements. The first is an array of return + values. The second is NIL if all calls succeeded. If a + call resulted in an error, it is a three-element array + with the zero-based index of the call which resulted in an + error, the error type and the error message. If an error + occurred, the values from all preceding calls will still + be returned. *nvim_parse_expression()* nvim_parse_expression({expr}, {flags}, {highlight}) @@ -930,20 +944,24 @@ nvim_parse_expression({expr}, {flags}, {highlight}) {async} Parameters: ~ - {expr} Expression to parse. Is always treated as a + {expr} Expression to parse. Always treated as a single line. - {flags} Flags: - "m" if multiple expressions in a - row are allowed (only the first one will be - parsed), - "E" if EOC tokens are not allowed - (determines whether they will stop parsing - process or be recognized as an - operator/space, though also yielding an - error). - "l" when needing to start parsing - with lvalues for ":let" or ":for". Common - flag sets: - "m" to parse like for ":echo". - - "E" to parse like for "=". - empty - string for ":call". - "lm" to parse for - ":let". + {flags} Flags: + - "m" if multiple expressions in a row are + allowed (only the first one will be + parsed), + - "E" if EOC tokens are not allowed + (determines whether they will stop parsing + process or be recognized as an + operator/space, though also yielding an + error). + - "l" when needing to start parsing with + lvalues for ":let" or ":for". Common flag + sets: + - "m" to parse like for ":echo". + - "E" to parse like for "=". + - empty string for ":call". + - "lm" to parse for ":let". {highlight} If true, return value will also include "highlight" key containing array of 4-tuples (arrays) (Integer, Integer, Integer, String), @@ -954,51 +972,66 @@ nvim_parse_expression({expr}, {flags}, {highlight}) [start_col, end_col)). Return: ~ - AST: top-level dictionary with these keys: "error": - Dictionary with error, present only if parser saw some - error. Contains the following keys: "message": String, - error message in printf format, translated. Must contain - exactly one "%.*s". "arg": String, error message argument. - "len": Amount of bytes successfully parsed. With flags - equal to "" that should be equal to the length of expr - string. @note: “Sucessfully parsed” here means - “participated in AST creation”, not “till the first - error”. "ast": AST, either nil or a dictionary with these - keys: "type": node type, one of the value names from - ExprASTNodeType stringified without "kExprNode" prefix. - "start": a pair [line, column] describing where node is - “started” where "line" is always 0 (will not be 0 if you - will be using nvim_parse_viml() on e.g. ":let", but that - is not present yet). Both elements are Integers. "len": - “length” of the node. This and "start" are there for - debugging purposes primary (debugging parser and providing - debug information). "children": a list of nodes described - in top/"ast". There always is zero, one or two children, - key will not be present if node has no children. Maximum - number of children may be found in node_maxchildren array. - Local values (present only for certain nodes): "scope": a - single Integer, specifies scope for "Option" and - "PlainIdentifier" nodes. For "Option" it is one of - ExprOptScope values, for "PlainIdentifier" it is one of - ExprVarScope values. "ident": identifier (without scope, - if any), present for "Option", "PlainIdentifier", - "PlainKey" and "Environment" nodes. "name": Integer, - register name (one character) or -1. Only present for - "Register" nodes. "cmp_type": String, comparison type, one - of the value names from ExprComparisonType, stringified - without "kExprCmp" prefix. Only present for "Comparison" - nodes. "ccs_strategy": String, case comparison strategy, - one of the value names from ExprCaseCompareStrategy, - stringified without "kCCStrategy" prefix. Only present for - "Comparison" nodes. "augmentation": String, augmentation - type for "Assignment" nodes. Is either an empty string, - "Add", "Subtract" or "Concat" for "=", "+=", "-=" or ".=" - respectively. "invert": Boolean, true if result of - comparison needs to be inverted. Only present for - "Comparison" nodes. "ivalue": Integer, integer value for - "Integer" nodes. "fvalue": Float, floating-point value for - "Float" nodes. "svalue": String, value for - "SingleQuotedString" and "DoubleQuotedString" nodes. + + - AST: top-level dictionary with these keys: + - "error": Dictionary with error, present only if parser + saw some error. Contains the following keys: + - "message": String, error message in printf format, + translated. Must contain exactly one "%.*s". + - "arg": String, error message argument. + + - "len": Amount of bytes successfully parsed. With flags + equal to "" that should be equal to the length of expr + string. (“Sucessfully parsed” here means “participated + in AST creation”, not “till the first error”.) + - "ast": AST, either nil or a dictionary with these + keys: + - "type": node type, one of the value names from + ExprASTNodeType stringified without "kExprNode" + prefix. + - "start": a pair [line, column] describing where node + is "started" where "line" is always 0 (will not be 0 + if you will be using nvim_parse_viml() on e.g. + ":let", but that is not present yet). Both elements + are Integers. + - "len": “length” of the node. This and "start" are + there for debugging purposes primary (debugging + parser and providing debug information). + - "children": a list of nodes described in top/"ast". + There always is zero, one or two children, key will + not be present if node has no children. Maximum + number of children may be found in node_maxchildren + array. + + - Local values (present only for certain nodes): + - "scope": a single Integer, specifies scope for + "Option" and "PlainIdentifier" nodes. For "Option" it + is one of ExprOptScope values, for "PlainIdentifier" + it is one of ExprVarScope values. + - "ident": identifier (without scope, if any), present + for "Option", "PlainIdentifier", "PlainKey" and + "Environment" nodes. + - "name": Integer, register name (one character) or -1. + Only present for "Register" nodes. + - "cmp_type": String, comparison type, one of the value + names from ExprComparisonType, stringified without + "kExprCmp" prefix. Only present for "Comparison" + nodes. + - "ccs_strategy": String, case comparison strategy, one + of the value names from ExprCaseCompareStrategy, + stringified without "kCCStrategy" prefix. Only present + for "Comparison" nodes. + - "augmentation": String, augmentation type for + "Assignment" nodes. Is either an empty string, "Add", + "Subtract" or "Concat" for "=", "+=", "-=" or ".=" + respectively. + - "invert": Boolean, true if result of comparison needs + to be inverted. Only present for "Comparison" nodes. + - "ivalue": Integer, integer value for "Integer" nodes. + - "fvalue": Float, floating-point value for "Float" + nodes. + - "svalue": String, value for "SingleQuotedString" and + "DoubleQuotedString" nodes. nvim__id({obj}) *nvim__id()* Returns object given as argument. @@ -1058,23 +1091,22 @@ nvim_list_uis() *nvim_list_uis()* Gets a list of dictionaries representing attached UIs. Return: ~ - Array of UI dictionaries. Each dictionary has the - following keys: - `height`: requested height of the UI - `width`: requested width of the UI - `rgb`: whether the UI uses rgb colors - (false implies cterm colors) - `ext_...`: Requested UI extensions, see |ui-options| - `chan`: Channel id of remote UI (not present for TUI) + Array of UI dictionaries, each with these keys: + - "height" requested height of the UI + - "width" requested width of the UI + - "rgb" whether the UI uses rgb colors (false implies + cterm colors) + - "ext_..." Requested UI extensions, see |ui-options| + - "chan" Channel id of remote UI (not present for TUI) nvim_get_proc_children({pid}) *nvim_get_proc_children()* - Gets the immediate children of process `pid`. + Gets the immediate children of process `pid` . Return: ~ Array of child process ids, empty if process not found. nvim_get_proc({pid}) *nvim_get_proc()* - Gets info describing process `pid`. + Gets info describing process `pid` . Return: ~ Map of process properties, or NIL if process not found. @@ -1096,7 +1128,7 @@ nvim_select_popupmenu_item({item}, {insert}, {finish}, {opts}) {insert} Whether the selection should be inserted in the buffer. {finish} Finish the completion and dismiss the popupmenu. - Implies `insert`. + Implies `insert` . {opts} Optional parameters. Reserved for future use. nvim__inspect_cell({row}, {col}) *nvim__inspect_cell()* @@ -1123,7 +1155,7 @@ nvim_buf_line_count({buffer}) *nvim_buf_line_count()* Gets the buffer line count Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer Return: ~ Line count, or 0 for unloaded buffer. |api-buffer| @@ -1132,11 +1164,11 @@ nvim_buf_attach({buffer}, {send_buffer}, {opts}) *nvim_buf_attach()* Activate updates from this buffer to the current channel. Parameters: ~ - {buffer} The buffer handle + {buffer} Buffer handle, or 0 for current buffer {send_buffer} Set to true if the initial notification should contain the whole buffer. If so, the first notification will be a - `nvim_buf_lines_event`. Otherwise, the + `nvim_buf_lines_event` . Otherwise, the first notification will be a `nvim_buf_changedtick_event` {opts} Optional parameters. Reserved for future @@ -1144,14 +1176,14 @@ nvim_buf_attach({buffer}, {send_buffer}, {opts}) *nvim_buf_attach()* Return: ~ False when updates couldn't be enabled because the buffer - isn't loaded or optscontained an invalid key; otherwise + isn't loaded or `opts` contained an invalid key; otherwise True. nvim_buf_detach({buffer}) *nvim_buf_detach()* Deactivate updates from this buffer to the current channel. Parameters: ~ - {buffer} The buffer handle + {buffer} Buffer handle, or 0 for current buffer Return: ~ False when updates couldn't be disabled because the buffer @@ -1169,7 +1201,7 @@ nvim_buf_get_lines({buffer}, {start}, {end}, {strict_indexing}) unless `strict_indexing` is set. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {start} First line index {end} Last line index (exclusive) {strict_indexing} Whether out-of-bounds should be an @@ -1196,7 +1228,7 @@ nvim_buf_set_lines({buffer}, {start}, {end}, {strict_indexing}, unless `strict_indexing` is set. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {start} First line index {end} Last line index (exclusive) {strict_indexing} Whether out-of-bounds should be an @@ -1216,7 +1248,7 @@ nvim_buf_get_offset({buffer}, {index}) *nvim_buf_get_offset()* Returns -1 for unloaded buffer. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {index} Line index Return: ~ @@ -1226,7 +1258,7 @@ nvim_buf_get_var({buffer}, {name}) *nvim_buf_get_var()* Gets a buffer-scoped (b:) variable. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Variable name Return: ~ @@ -1236,17 +1268,17 @@ nvim_buf_get_changedtick({buffer}) *nvim_buf_get_changedtick()* Gets a changed tick of a buffer Parameters: ~ - {buffer} Buffer handle. + {buffer} Buffer handle, or 0 for current buffer Return: ~ - b:changedtickvalue. + `b:changedtick` value. nvim_buf_get_keymap({buffer}, {mode}) *nvim_buf_get_keymap()* Gets a list of buffer-local |mapping| definitions. Parameters: ~ {mode} Mode short-name ("n", "i", "v", ...) - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer Return: ~ Array of maparg()-like dictionaries describing mappings. @@ -1256,7 +1288,7 @@ nvim_buf_get_commands({buffer}, {opts}) *nvim_buf_get_commands()* Gets a map of buffer-local |user-commands|. Parameters: ~ - {buffer} Buffer handle. + {buffer} Buffer handle, or 0 for current buffer {opts} Optional parameters. Currently not used. Return: ~ @@ -1266,7 +1298,7 @@ nvim_buf_set_var({buffer}, {name}, {value}) *nvim_buf_set_var()* Sets a buffer-scoped (b:) variable Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Variable name {value} Variable value @@ -1274,14 +1306,14 @@ nvim_buf_del_var({buffer}, {name}) *nvim_buf_del_var()* Removes a buffer-scoped (b:) variable Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Variable name nvim_buf_get_option({buffer}, {name}) *nvim_buf_get_option()* Gets a buffer option value Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Option name Return: ~ @@ -1292,7 +1324,7 @@ nvim_buf_set_option({buffer}, {name}, {value}) *nvim_buf_set_option()* option (only works if there's a global fallback) Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Option name {value} Option value @@ -1300,7 +1332,7 @@ nvim_buf_get_name({buffer}) *nvim_buf_get_name()* Gets the full file name for the buffer Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer Return: ~ Buffer name @@ -1309,7 +1341,7 @@ nvim_buf_set_name({buffer}, {name}) *nvim_buf_set_name()* Sets the full file name for a buffer Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Buffer name nvim_buf_is_loaded({buffer}) *nvim_buf_is_loaded()* @@ -1317,7 +1349,7 @@ nvim_buf_is_loaded({buffer}) *nvim_buf_is_loaded()* more info about unloaded buffers. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer Return: ~ true if the buffer is valid and loaded, false otherwise. @@ -1330,7 +1362,7 @@ nvim_buf_is_valid({buffer}) *nvim_buf_is_valid()* |api-buffer| for more info about unloaded buffers. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer Return: ~ true if the buffer is valid, false otherwise. @@ -1340,7 +1372,7 @@ nvim_buf_get_mark({buffer}, {name}) *nvim_buf_get_mark()* named mark Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {name} Mark name Return: ~ @@ -1364,7 +1396,7 @@ nvim_buf_add_highlight({buffer}, {ns_id}, {hl_group}, {line}, `ns_id` to add highlights to the namespace. All highlights in the same namespace can then be cleared with single call to |nvim_buf_clear_namespace|. If the highlight never will be - deleted by an API call, pass `ns_id = -1`. + deleted by an API call, pass `ns_id = -1` . As a shorthand, `ns_id = 0` can be used to create a new namespace for the highlight, the allocated id is then @@ -1374,7 +1406,7 @@ nvim_buf_add_highlight({buffer}, {ns_id}, {hl_group}, {line}, |nvim_create_namespace| to create a new empty namespace. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {ns_id} namespace to use or -1 for ungrouped highlight {hl_group} Name of the highlight group to use @@ -1396,7 +1428,7 @@ nvim_buf_clear_namespace({buffer}, {ns_id}, {line_start}, {line_end}) to line_start and line_end respectively. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {ns_id} Namespace to clear, or -1 to clear all namespaces. {line_start} Start of range of lines to clear @@ -1420,14 +1452,14 @@ nvim_buf_set_virtual_text({buffer}, {ns_id}, {line}, {chunks}, {opts}) both virtual text and highlights added by |nvim_buf_add_highlight|, both can then be cleared with a single call to |nvim_buf_clear_namespace|. If the virtual text - never will be cleared by an API call, pass `ns_id = -1`. + never will be cleared by an API call, pass `ns_id = -1` . As a shorthand, `ns_id = 0` can be used to create a new namespace for the virtual text, the allocated id is then returned. Parameters: ~ - {buffer} Buffer handle + {buffer} Buffer handle, or 0 for current buffer {ns_id} Namespace to use or 0 to create a namespace, or -1 for a ungrouped annotation {line} Line to annotate with virtual text @@ -1601,7 +1633,7 @@ nvim_win_set_config({window}, {config}) *nvim_win_set_config()* parameters. When reconfiguring a floating window, absent option keys will - not be changed. The following restriction apply: `row`, `col` + not be changed. The following restriction apply: `row` , `col` and `relative` must be reconfigured together. Only changing a subset of these is an error. @@ -1636,9 +1668,6 @@ nvim_win_close({window}, {force}) *nvim_win_close()* buffer will become hidden, even if 'hidden' is not set. - Return: ~ - Window number - ============================================================================== Tabpage Functions *api-tabpage* @@ -1650,7 +1679,7 @@ nvim_tabpage_list_wins({tabpage}) *nvim_tabpage_list_wins()* {tabpage} Tabpage Return: ~ - List of windows in tabpage + List of windows in `tabpage` nvim_tabpage_get_var({tabpage}, {name}) *nvim_tabpage_get_var()* Gets a tab-scoped (t:) variable diff --git a/scripts/gen_api_vimdoc.py b/scripts/gen_api_vimdoc.py deleted file mode 100755 index 515964bfe8..0000000000 --- a/scripts/gen_api_vimdoc.py +++ /dev/null @@ -1,563 +0,0 @@ -#!/usr/bin/env python3 -"""Parses Doxygen XML output to generate Neovim's API documentation. - -This would be easier using lxml and XSLT, but: - - 1. This should avoid needing Python dependencies, especially ones that are - C modules that have library dependencies (lxml requires libxml and - libxslt). - 2. I wouldn't know how to deal with nested indentation in tags using - XSLT. - -Each function documentation is formatted with the following rules: - - - Maximum width of 78 characters (`text_width`). - - Spaces for indentation. - - Function signature and helptag are on the same line. - - Helptag is right aligned. - - Signature and helptag must have a minimum of 8 spaces between them. - - If the signature is too long, it is placed on the line after the - helptag. The signature wraps at `text_width - 8` characters with - subsequent lines indented to the open parenthesis. - - Documentation body will be indented by 16 spaces. - - Subsection bodies are indented an additional 4 spaces. - - Documentation body consists of the function description, parameter details, - return description, and C declaration. - - Parameters are omitted for the `void` and `Error *` types, or if the - parameter is marked as [out]. - - Each function documentation is separated by a single line. - -The C declaration is added to the end to show actual argument types. -""" -import os -import re -import sys -import shutil -import textwrap -import subprocess - -from xml.dom import minidom - -if sys.version_info[0] < 3: - print("use Python 3") - sys.exit(1) - -doc_filename = 'api.txt' -# String used to find the start of the generated part of the doc. -section_start_token = '*api-global*' -# Required prefix for API function names. -api_func_name_prefix = 'nvim_' - -# Section name overrides. -section_name = { - 'vim.c': 'Global', -} - -# Section ordering. -section_order = ( - 'vim.c', - 'buffer.c', - 'window.c', - 'tabpage.c', - 'ui.c', -) - -param_exclude = ( - 'channel_id', -) - -# Annotations are displayed as line items after API function descriptions. -annotation_map = { - 'FUNC_API_ASYNC': '{async}', -} - -text_width = 78 -script_path = os.path.abspath(__file__) -base_dir = os.path.dirname(os.path.dirname(script_path)) -src_dir = os.path.join(base_dir, 'src/nvim/api') -out_dir = os.path.join(base_dir, 'tmp-api-doc') -filter_cmd = '%s %s' % (sys.executable, script_path) -seen_funcs = set() - -# Tracks `xrefsect` titles. As of this writing, used only for separating -# deprecated functions. -xrefs = set() - - -# XML Parsing Utilities {{{ -def find_first(parent, name): - """Finds the first matching node within parent.""" - sub = parent.getElementsByTagName(name) - if not sub: - return None - return sub[0] - - -def get_children(parent, name): - """Yield matching child nodes within parent.""" - for child in parent.childNodes: - if child.nodeType == child.ELEMENT_NODE and child.nodeName == name: - yield child - - -def get_child(parent, name): - """Get the first matching child node.""" - for child in get_children(parent, name): - return child - return None - - -def clean_text(text): - """Cleans text. - - Only cleans superfluous whitespace at the moment. - """ - return ' '.join(text.split()).strip() - - -def clean_lines(text): - """Removes superfluous lines. - - The beginning and end of the string is trimmed. Empty lines are collapsed. - """ - return re.sub(r'\A\n\s*\n*|\n\s*\n*\Z', '', re.sub(r'(\n\s*\n+)+', '\n\n', text)) - - -def get_text(parent): - """Combine all text in a node.""" - if parent.nodeType == parent.TEXT_NODE: - return parent.data - - out = '' - for node in parent.childNodes: - if node.nodeType == node.TEXT_NODE: - out += clean_text(node.data) - elif node.nodeType == node.ELEMENT_NODE: - out += ' ' + get_text(node) - return out - - -def doc_wrap(text, prefix='', width=70, func=False): - """Wraps text to `width`. - - The first line is prefixed with `prefix`, and subsequent lines are aligned. - If `func` is True, only wrap at commas. - """ - if not width: - return text - - indent_space = ' ' * len(prefix) - - if func: - lines = [prefix] - for part in text.split(', '): - if part[-1] not in ');': - part += ', ' - if len(lines[-1]) + len(part) > width: - lines.append(indent_space) - lines[-1] += part - return '\n'.join(x.rstrip() for x in lines).rstrip() - - tw = textwrap.TextWrapper(break_long_words = False, - break_on_hyphens = False, - width=width, - initial_indent=prefix, - subsequent_indent=indent_space) - return '\n'.join(tw.wrap(text.strip())) - - -def parse_params(parent, width=62): - """Parse Doxygen `parameterlist`.""" - name_length = 0 - items = [] - for child in parent.childNodes: - if child.nodeType == child.TEXT_NODE: - continue - - name_node = find_first(child, 'parametername') - if name_node.getAttribute('direction') == 'out': - continue - - name = get_text(name_node) - if name in param_exclude: - continue - - name = '{%s}' % name - name_length = max(name_length, len(name) + 2) - - desc = '' - desc_node = get_child(child, 'parameterdescription') - if desc_node: - desc = parse_parblock(desc_node, width=None) - items.append((name.strip(), desc.strip())) - - out = 'Parameters: ~\n' - for name, desc in items: - name = ' %s' % name.ljust(name_length) - out += doc_wrap(desc, prefix=name, width=width) + '\n' - return out.strip() - - -def parse_para(parent, width=62): - """Parse doxygen `para` tag. - - I assume is a paragraph block or "a block of text". It can contain - text nodes, or other tags. - """ - line = '' - lines = [] - for child in parent.childNodes: - if child.nodeType == child.TEXT_NODE: - line += child.data - elif child.nodeName == 'computeroutput': - line += '`%s`' % get_text(child) - else: - if line: - lines.append(doc_wrap(line, width=width)) - line = '' - - if child.nodeName == 'parameterlist': - lines.append(parse_params(child, width=width)) - elif child.nodeName == 'xrefsect': - title = get_text(get_child(child, 'xreftitle')) - xrefs.add(title) - xrefdesc = parse_para(get_child(child, 'xrefdescription')) - lines.append(doc_wrap(xrefdesc, prefix='%s: ' % title, - width=width) + '\n') - elif child.nodeName == 'simplesect': - kind = child.getAttribute('kind') - if kind == 'note': - lines.append('Note:') - lines.append(doc_wrap(parse_para(child), - prefix=' ', - width=width)) - elif kind == 'return': - lines.append('%s: ~' % kind.title()) - lines.append(doc_wrap(parse_para(child), - prefix=' ', - width=width)) - else: - lines.append(get_text(child)) - - if line: - lines.append(doc_wrap(line, width=width)) - return clean_lines('\n'.join(lines).strip()) - - -def parse_parblock(parent, width=62): - """Parses a nested block of `para` tags. - - Named after the \parblock command, but not directly related. - """ - paragraphs = [] - for child in parent.childNodes: - if child.nodeType == child.TEXT_NODE: - paragraphs.append(doc_wrap(child.data, width=width)) - elif child.nodeName == 'para': - paragraphs.append(parse_para(child, width=width)) - else: - paragraphs.append(doc_wrap(get_text(child), width=width)) - paragraphs.append('') - return clean_lines('\n'.join(paragraphs).strip()) -# }}} - - -def parse_source_xml(filename): - """Collects API functions. - - Returns two strings: - 1. API functions - 2. Deprecated API functions - - Caller decides what to do with the deprecated documentation. - """ - global xrefs - xrefs = set() - functions = [] - deprecated_functions = [] - - dom = minidom.parse(filename) - for member in dom.getElementsByTagName('memberdef'): - if member.getAttribute('static') == 'yes' or \ - member.getAttribute('kind') != 'function': - continue - - loc = find_first(member, 'location') - if 'private' in loc.getAttribute('file'): - continue - - return_type = get_text(get_child(member, 'type')) - if return_type == '': - continue - - if return_type.startswith(('ArrayOf', 'DictionaryOf')): - parts = return_type.strip('_').split('_') - return_type = '%s(%s)' % (parts[0], ', '.join(parts[1:])) - - name = get_text(get_child(member, 'name')) - - annotations = get_text(get_child(member, 'argsstring')) - if annotations and ')' in annotations: - annotations = annotations.rsplit(')', 1)[-1].strip() - # XXX: (doxygen 1.8.11) 'argsstring' only includes attributes of - # non-void functions. Special-case void functions here. - if name == 'nvim_get_mode' and len(annotations) == 0: - annotations += 'FUNC_API_ASYNC' - annotations = filter(None, map(lambda x: annotation_map.get(x), - annotations.split())) - - vimtag = '*%s()*' % name - args = [] - type_length = 0 - - for param in get_children(member, 'param'): - arg_type = get_text(get_child(param, 'type')).strip() - arg_name = '' - declname = get_child(param, 'declname') - if declname: - arg_name = get_text(declname).strip() - - if arg_name in param_exclude: - continue - - if arg_type.endswith('*'): - arg_type = arg_type.strip('* ') - arg_name = '*' + arg_name - type_length = max(type_length, len(arg_type)) - args.append((arg_type, arg_name)) - - c_args = [] - for arg_type, arg_name in args: - c_args.append(' ' + ( - '%s %s' % (arg_type.ljust(type_length), arg_name)).strip()) - - c_decl = textwrap.indent('%s %s(\n%s\n);' % (return_type, name, - ',\n'.join(c_args)), - ' ') - - prefix = '%s(' % name - suffix = '%s)' % ', '.join('{%s}' % a[1] for a in args - if a[0] not in ('void', 'Error')) - - # Minimum 8 chars between signature and vimtag - lhs = (text_width - 8) - len(prefix) - - if len(prefix) + len(suffix) > lhs: - signature = vimtag.rjust(text_width) + '\n' - signature += doc_wrap(suffix, width=text_width-8, prefix=prefix, - func=True) - else: - signature = prefix + suffix - signature += vimtag.rjust(text_width - len(signature)) - - doc = '' - desc = find_first(member, 'detaileddescription') - if desc: - doc = parse_parblock(desc) - if 'DEBUG' in os.environ: - print(textwrap.indent( - re.sub(r'\n\s*\n+', '\n', - desc.toprettyxml(indent=' ', newl='\n')), ' ' * 16)) - - if not doc: - doc = 'TODO: Documentation' - - annotations = '\n'.join(annotations) - if annotations: - annotations = ('\n\nAttributes: ~\n' + - textwrap.indent(annotations, ' ')) - i = doc.rfind('Parameters: ~') - if i == -1: - doc += annotations - else: - doc = doc[:i] + annotations + '\n\n' + doc[i:] - - if 'INCLUDE_C_DECL' in os.environ: - doc += '\n\nC Declaration: ~\n>\n' - doc += c_decl - doc += '\n<' - - func_doc = signature + '\n' - func_doc += textwrap.indent(clean_lines(doc), ' ' * 16) - func_doc = re.sub(r'^\s+([<>])$', r'\1', func_doc, flags=re.M) - - if 'Deprecated' in xrefs: - deprecated_functions.append(func_doc) - elif name.startswith(api_func_name_prefix): - functions.append(func_doc) - - xrefs.clear() - - return '\n\n'.join(functions), '\n\n'.join(deprecated_functions) - - -def delete_lines_below(filename, tokenstr): - """Deletes all lines below the line containing `tokenstr`, the line itself, - and one line above it. - """ - lines = open(filename).readlines() - i = 0 - for i, line in enumerate(lines, 1): - if tokenstr in line: - break - i = max(0, i - 2) - with open(filename, 'wt') as fp: - fp.writelines(lines[0:i]) - -def gen_docs(config): - """Generate documentation. - - Doxygen is called and configured through stdin. - """ - p = subprocess.Popen(['doxygen', '-'], stdin=subprocess.PIPE) - p.communicate(config.format(input=src_dir, output=out_dir, - filter=filter_cmd).encode('utf8')) - if p.returncode: - sys.exit(p.returncode) - - sections = {} - intros = {} - sep = '=' * text_width - - base = os.path.join(out_dir, 'xml') - dom = minidom.parse(os.path.join(base, 'index.xml')) - - # generate docs for section intros - for compound in dom.getElementsByTagName('compound'): - if compound.getAttribute('kind') != 'group': - continue - - groupname = get_text(find_first(compound, 'name')) - groupxml = os.path.join(base, '%s.xml' % compound.getAttribute('refid')) - - desc = find_first(minidom.parse(groupxml), 'detaileddescription') - if desc: - doc = parse_parblock(desc) - if doc: - intros[groupname] = doc - - for compound in dom.getElementsByTagName('compound'): - if compound.getAttribute('kind') != 'file': - continue - - filename = get_text(find_first(compound, 'name')) - if filename.endswith('.c'): - functions, deprecated = parse_source_xml( - os.path.join(base, '%s.xml' % compound.getAttribute('refid'))) - - if not functions and not deprecated: - continue - - if functions or deprecated: - name = os.path.splitext(os.path.basename(filename))[0] - if name == 'ui': - name = name.upper() - else: - name = name.title() - - doc = '' - - intro = intros.get('api-%s' % name.lower()) - if intro: - doc += '\n\n' + intro - - if functions: - doc += '\n\n' + functions - - if 'INCLUDE_DEPRECATED' in os.environ and deprecated: - doc += '\n\n\nDeprecated %s Functions: ~\n\n' % name - doc += deprecated - - if doc: - filename = os.path.basename(filename) - name = section_name.get(filename, name) - title = '%s Functions' % name - helptag = '*api-%s*' % name.lower() - sections[filename] = (title, helptag, doc) - - if not sections: - return - - docs = '' - - i = 0 - for filename in section_order: - if filename not in sections: - continue - title, helptag, section_doc = sections.pop(filename) - - i += 1 - docs += sep - docs += '\n%s%s' % (title, helptag.rjust(text_width - len(title))) - docs += section_doc - docs += '\n\n\n' - - if sections: - # In case new API sources are added without updating the order dict. - for title, helptag, section_doc in sections.values(): - i += 1 - docs += sep - docs += '\n%s%s' % (title, helptag.rjust(text_width - len(title))) - docs += section_doc - docs += '\n\n\n' - - docs = docs.rstrip() + '\n\n' - docs += ' vim:tw=78:ts=8:ft=help:norl:\n' - - doc_file = os.path.join(base_dir, 'runtime/doc', doc_filename) - delete_lines_below(doc_file, section_start_token) - with open(doc_file, 'ab') as fp: - fp.write(docs.encode('utf8')) - shutil.rmtree(out_dir) - - -def filter_source(filename): - """Filters the source to fix macros that confuse Doxygen.""" - with open(filename, 'rt') as fp: - print(re.sub(r'^(ArrayOf|DictionaryOf)(\(.*?\))', - lambda m: m.group(1)+'_'.join( - re.split(r'[^\w]+', m.group(2))), - fp.read(), flags=re.M)) - - -# Doxygen Config {{{ -Doxyfile = ''' -OUTPUT_DIRECTORY = {output} -INPUT = {input} -INPUT_ENCODING = UTF-8 -FILE_PATTERNS = *.h *.c -RECURSIVE = YES -INPUT_FILTER = "{filter}" -EXCLUDE = -EXCLUDE_SYMLINKS = NO -EXCLUDE_PATTERNS = */private/* -EXCLUDE_SYMBOLS = - -GENERATE_HTML = NO -GENERATE_DOCSET = NO -GENERATE_HTMLHELP = NO -GENERATE_QHP = NO -GENERATE_TREEVIEW = NO -GENERATE_LATEX = NO -GENERATE_RTF = NO -GENERATE_MAN = NO -GENERATE_DOCBOOK = NO -GENERATE_AUTOGEN_DEF = NO - -GENERATE_XML = YES -XML_OUTPUT = xml -XML_PROGRAMLISTING = NO - -ENABLE_PREPROCESSING = YES -MACRO_EXPANSION = YES -EXPAND_ONLY_PREDEF = NO -''' -# }}} - -if __name__ == "__main__": - if len(sys.argv) > 1: - filter_source(sys.argv[1]) - else: - gen_docs(Doxyfile) - -# vim: set ft=python ts=4 sw=4 tw=79 et fdm=marker : diff --git a/scripts/gen_vimdoc.py b/scripts/gen_vimdoc.py new file mode 100755 index 0000000000..a62d18f02e --- /dev/null +++ b/scripts/gen_vimdoc.py @@ -0,0 +1,690 @@ +#!/usr/bin/env python3 +"""Generates Nvim help docs from C docstrings, by parsing Doxygen XML. + +This would be easier using lxml and XSLT, but: + + 1. This should avoid needing Python dependencies, especially ones that are + C modules that have library dependencies (lxml requires libxml and + libxslt). + 2. I wouldn't know how to deal with nested indentation in tags using + XSLT. + +Each function documentation is formatted with the following rules: + + - Maximum width of 78 characters (`text_width`). + - Spaces for indentation. + - Function signature and helptag are on the same line. + - Helptag is right aligned. + - Signature and helptag must have a minimum of 8 spaces between them. + - If the signature is too long, it is placed on the line after the + helptag. The signature wraps at `text_width - 8` characters with + subsequent lines indented to the open parenthesis. + - Documentation body will be indented by 16 spaces. + - Subsection bodies are indented an additional 4 spaces. + - Documentation body consists of the function description, parameter details, + return description, and C declaration. + - Parameters are omitted for the `void` and `Error *` types, or if the + parameter is marked as [out]. + - Each function documentation is separated by a single line. + +The C declaration is added to the end to show actual argument types. +""" +import os +import re +import sys +import shutil +import textwrap +import subprocess +import collections +import pprint + +from xml.dom import minidom + +if sys.version_info[0] < 3: + print("use Python 3") + sys.exit(1) + +DEBUG = ('DEBUG' in os.environ) +INCLUDE_C_DECL = ('INCLUDE_C_DECL' in os.environ) +INCLUDE_DEPRECATED = ('INCLUDE_DEPRECATED' in os.environ) + +doc_filename = 'api.txt' +# String used to find the start of the generated part of the doc. +section_start_token = '*api-global*' +# Required prefix for API function names. +api_func_name_prefix = 'nvim_' + +# Section name overrides. +section_name = { + 'vim.c': 'Global', +} + +# Section ordering. +section_order = ( + 'vim.c', + 'buffer.c', + 'window.c', + 'tabpage.c', + 'ui.c', +) + +param_exclude = ( + 'channel_id', +) + +# Annotations are displayed as line items after API function descriptions. +annotation_map = { + 'FUNC_API_ASYNC': '{async}', +} + +text_width = 78 +script_path = os.path.abspath(__file__) +base_dir = os.path.dirname(os.path.dirname(script_path)) +src_dir = os.path.join(base_dir, 'src/nvim/api') +out_dir = os.path.join(base_dir, 'tmp-api-doc') +filter_cmd = '%s %s' % (sys.executable, script_path) +seen_funcs = set() + +# Tracks `xrefsect` titles. As of this writing, used only for separating +# deprecated functions. +xrefs = set() + +def debug_this(s, n): + o = n if isinstance(n, str) else n.toprettyxml(indent=' ', newl='\n') + name = '' if isinstance(n, str) else n.nodeName + if s in o: + raise RuntimeError('xxx: {}\n{}'.format(name, o)) + + +# XML Parsing Utilities {{{ +def find_first(parent, name): + """Finds the first matching node within parent.""" + sub = parent.getElementsByTagName(name) + if not sub: + return None + return sub[0] + + +def get_children(parent, name): + """Yield matching child nodes within parent.""" + for child in parent.childNodes: + if child.nodeType == child.ELEMENT_NODE and child.nodeName == name: + yield child + + +def get_child(parent, name): + """Get the first matching child node.""" + for child in get_children(parent, name): + return child + return None + + +def clean_text(text): + """Cleans text. + + Only cleans superfluous whitespace at the moment. + """ + return ' '.join(text.split()).strip() + + +def clean_lines(text): + """Removes superfluous lines. + + The beginning and end of the string is trimmed. Empty lines are collapsed. + """ + return re.sub(r'\A\n\s*\n*|\n\s*\n*\Z', '', re.sub(r'(\n\s*\n+)+', '\n\n', text)) + + +def is_blank(text): + return '' == clean_lines(text) + + +def get_text(parent): + """Combine all text in a node.""" + if parent.nodeType == parent.TEXT_NODE: + return parent.data + + out = '' + for node in parent.childNodes: + if node.nodeType == node.TEXT_NODE: + out += clean_text(node.data) + elif node.nodeType == node.ELEMENT_NODE: + out += ' ' + get_text(node) + return out + + +# Gets the length of the last line in `text`, excluding newline ("\n") char. +def len_lastline(text): + lastnl = text.rfind('\n') + if -1 == lastnl: + return len(text) + if '\n' == text[-1]: + return lastnl - (1+ text.rfind('\n', 0, lastnl)) + return len(text) - (1 + lastnl) + + +def len_lastline_withoutindent(text, indent): + n = len_lastline(text) + return (n - len(indent)) if n > len(indent) else 0 + + +# Returns True if node `n` contains only inline (not block-level) elements. +def is_inline(n): + for c in n.childNodes: + if c.nodeType != c.TEXT_NODE and c.nodeName != 'computeroutput': + return False + if not is_inline(c): + return False + return True + +def doc_wrap(text, prefix='', width=70, func=False, indent=None): + """Wraps text to `width`. + + First line is prefixed with `prefix`, subsequent lines are aligned. + If `func` is True, only wrap at commas. + """ + if not width: + # return prefix + text + return text + + # Whitespace used to indent all lines except the first line. + indent = ' ' * len(prefix) if indent is None else indent + indent_only = (prefix == '' and indent is not None) + + if func: + lines = [prefix] + for part in text.split(', '): + if part[-1] not in ');': + part += ', ' + if len(lines[-1]) + len(part) > width: + lines.append(indent) + lines[-1] += part + return '\n'.join(x.rstrip() for x in lines).rstrip() + + # XXX: Dummy prefix to force TextWrapper() to wrap the first line. + if indent_only: + prefix = indent + + tw = textwrap.TextWrapper(break_long_words = False, + break_on_hyphens = False, + width=width, + initial_indent=prefix, + subsequent_indent=indent) + result = '\n'.join(tw.wrap(text.strip())) + + # XXX: Remove the dummy prefix. + if indent_only: + result = result[len(indent):] + + return result + + +def render_params(parent, width=62): + """Renders Doxygen tag as Vim help text.""" + name_length = 0 + items = [] + for node in parent.childNodes: + if node.nodeType == node.TEXT_NODE: + continue + + name_node = find_first(node, 'parametername') + if name_node.getAttribute('direction') == 'out': + continue + + name = get_text(name_node) + if name in param_exclude: + continue + + name = '{%s}' % name + name_length = max(name_length, len(name) + 2) + items.append((name.strip(), node)) + + out = '' + for name, node in items: + name = ' {}'.format(name.ljust(name_length)) + + desc = '' + desc_node = get_child(node, 'parameterdescription') + if desc_node: + desc = parse_parblock(desc_node, width=width, + indent=(' ' * len(name))) + + out += '{}{}\n'.format(name, desc) + return out.rstrip() + +# Renders a node as Vim help text, recursively traversing all descendants. +def render_node(n, text, prefix='', indent='', width=62): + text = '' + # space_preceding = (len(text) > 0 and ' ' == text[-1][-1]) + # text += (int(not space_preceding) * ' ') + + if n.nodeType == n.TEXT_NODE: + # `prefix` is NOT sent to doc_wrap, it was already handled by now. + text += doc_wrap(n.data, indent=indent, width=width) + elif n.nodeName == 'computeroutput': + text += ' `{}` '.format(get_text(n)) + elif is_inline(n): + for c in n.childNodes: + text += render_node(c, text) + text = doc_wrap(text, indent=indent, width=width) + elif n.nodeName == 'verbatim': + # TODO: currently we don't use this. The "[verbatim]" hint is there as + # a reminder that we must decide how to format this if we do use it. + text += ' [verbatim] {}'.format(get_text(n)) + elif n.nodeName == 'listitem': + for c in n.childNodes: + text += indent + prefix + render_node(c, text, indent=indent+(' ' * len(prefix)), width=width) + elif n.nodeName == 'para': + for c in n.childNodes: + text += render_node(c, text, indent=indent, width=width) + if is_inline(n): + text = doc_wrap(text, indent=indent, width=width) + elif n.nodeName == 'itemizedlist': + for c in n.childNodes: + text += '{}\n'.format(render_node(c, text, prefix='- ', + indent=indent, width=width)) + elif n.nodeName == 'orderedlist': + i = 1 + for c in n.childNodes: + if is_blank(get_text(c)): + text += '\n' + continue + text += '{}\n'.format(render_node(c, text, prefix='{}. '.format(i), + indent=indent, width=width)) + i = i + 1 + elif n.nodeName == 'simplesect' and 'note' == n.getAttribute('kind'): + text += 'Note:\n ' + for c in n.childNodes: + text += render_node(c, text, indent=' ', width=width) + text += '\n' + elif n.nodeName == 'simplesect' and 'warning' == n.getAttribute('kind'): + text += 'Warning:\n ' + for c in n.childNodes: + text += render_node(c, text, indent=' ', width=width) + text += '\n' + elif (n.nodeName == 'simplesect' + and n.getAttribute('kind') in ('return', 'see')): + text += ' ' + for c in n.childNodes: + text += render_node(c, text, indent=' ', width=width) + else: + raise RuntimeError('unhandled node type: {}\n{}'.format( + n.nodeName, n.toprettyxml(indent=' ', newl='\n'))) + return text + +def render_para(parent, indent='', width=62): + """Renders Doxygen containing arbitrary nodes. + + NB: Blank lines in a docstring manifest as tags. + """ + if is_inline(parent): + return clean_lines(doc_wrap(render_node(parent, ''), + indent=indent, width=width).strip()) + + # Ordered dict of ordered lists. + groups = collections.OrderedDict([ + ('params', []), + ('return', []), + ('seealso', []), + ('xrefs', []), + ]) + + # Gather nodes into groups. Mostly this is because we want "parameterlist" + # nodes to appear together. + text = '' + kind = '' + last = '' + for child in parent.childNodes: + if child.nodeName == 'parameterlist': + groups['params'].append(child) + elif child.nodeName == 'xrefsect': + groups['xrefs'].append(child) + elif child.nodeName == 'simplesect': + last = kind + kind = child.getAttribute('kind') + if kind == 'return' or (kind == 'note' and last == 'return'): + groups['return'].append(child) + elif kind == 'see': + groups['seealso'].append(child) + elif kind in ('note', 'warning'): + text += render_node(child, text, indent=indent, width=width) + else: + raise RuntimeError('unhandled simplesect: {}\n{}'.format( + child.nodeName, child.toprettyxml(indent=' ', newl='\n'))) + else: + text += render_node(child, text, indent=indent, width=width) + + chunks = [text] + # Generate text from the gathered items. + if len(groups['params']) > 0: + chunks.append('\nParameters: ~') + for child in groups['params']: + chunks.append(render_params(child, width=width)) + if len(groups['return']) > 0: + chunks.append('\nReturn: ~') + for child in groups['return']: + chunks.append(render_node(child, chunks[-1][-1], indent=indent, width=width)) + if len(groups['seealso']) > 0: + chunks.append('\nSee also: ~') + for child in groups['seealso']: + chunks.append(render_node(child, chunks[-1][-1], indent=indent, width=width)) + for child in groups['xrefs']: + title = get_text(get_child(child, 'xreftitle')) + xrefs.add(title) + xrefdesc = render_para(get_child(child, 'xrefdescription'), width=width) + chunks.append(doc_wrap(xrefdesc, prefix='{}: '.format(title), + width=width) + '\n') + + return clean_lines('\n'.join(chunks).strip()) + + +def parse_parblock(parent, prefix='', width=62, indent=''): + """Renders a nested block of tags as Vim help text.""" + paragraphs = [] + for child in parent.childNodes: + paragraphs.append(render_para(child, width=width, indent=indent)) + paragraphs.append('') + return clean_lines('\n'.join(paragraphs).strip()) +# }}} + + +def parse_source_xml(filename): + """Collects API functions. + + Returns two strings: + 1. API functions + 2. Deprecated API functions + + Caller decides what to do with the deprecated documentation. + """ + global xrefs + xrefs = set() + functions = [] + deprecated_functions = [] + + dom = minidom.parse(filename) + for member in dom.getElementsByTagName('memberdef'): + if member.getAttribute('static') == 'yes' or \ + member.getAttribute('kind') != 'function': + continue + + loc = find_first(member, 'location') + if 'private' in loc.getAttribute('file'): + continue + + return_type = get_text(get_child(member, 'type')) + if return_type == '': + continue + + if return_type.startswith(('ArrayOf', 'DictionaryOf')): + parts = return_type.strip('_').split('_') + return_type = '{}({})'.format(parts[0], ', '.join(parts[1:])) + + name = get_text(get_child(member, 'name')) + + annotations = get_text(get_child(member, 'argsstring')) + if annotations and ')' in annotations: + annotations = annotations.rsplit(')', 1)[-1].strip() + # XXX: (doxygen 1.8.11) 'argsstring' only includes attributes of + # non-void functions. Special-case void functions here. + if name == 'nvim_get_mode' and len(annotations) == 0: + annotations += 'FUNC_API_ASYNC' + annotations = filter(None, map(lambda x: annotation_map.get(x), + annotations.split())) + + vimtag = '*{}()*'.format(name) + params = [] + type_length = 0 + + for param in get_children(member, 'param'): + param_type = get_text(get_child(param, 'type')).strip() + param_name = '' + declname = get_child(param, 'declname') + if declname: + param_name = get_text(declname).strip() + + if param_name in param_exclude: + continue + + if param_type.endswith('*'): + param_type = param_type.strip('* ') + param_name = '*' + param_name + type_length = max(type_length, len(param_type)) + params.append((param_type, param_name)) + + c_args = [] + for param_type, param_name in params: + c_args.append(' ' + ( + '%s %s' % (param_type.ljust(type_length), param_name)).strip()) + + c_decl = textwrap.indent('%s %s(\n%s\n);' % (return_type, name, + ',\n'.join(c_args)), + ' ') + + prefix = '%s(' % name + suffix = '%s)' % ', '.join('{%s}' % a[1] for a in params + if a[0] not in ('void', 'Error')) + + # Minimum 8 chars between signature and vimtag + lhs = (text_width - 8) - len(prefix) + + if len(prefix) + len(suffix) > lhs: + signature = vimtag.rjust(text_width) + '\n' + signature += doc_wrap(suffix, width=text_width-8, prefix=prefix, + func=True) + else: + signature = prefix + suffix + signature += vimtag.rjust(text_width - len(signature)) + + doc = '' + desc = find_first(member, 'detaileddescription') + if desc: + doc = parse_parblock(desc) + if DEBUG: + print(textwrap.indent( + re.sub(r'\n\s*\n+', '\n', + desc.toprettyxml(indent=' ', newl='\n')), ' ' * 16)) + + if not doc: + doc = 'TODO: Documentation' + + annotations = '\n'.join(annotations) + if annotations: + annotations = ('\n\nAttributes: ~\n' + + textwrap.indent(annotations, ' ')) + i = doc.rfind('Parameters: ~') + if i == -1: + doc += annotations + else: + doc = doc[:i] + annotations + '\n\n' + doc[i:] + + if INCLUDE_C_DECL: + doc += '\n\nC Declaration: ~\n>\n' + doc += c_decl + doc += '\n<' + + func_doc = signature + '\n' + func_doc += textwrap.indent(clean_lines(doc), ' ' * 16) + func_doc = re.sub(r'^\s+([<>])$', r'\1', func_doc, flags=re.M) + + if 'Deprecated' in xrefs: + deprecated_functions.append(func_doc) + elif name.startswith(api_func_name_prefix): + functions.append(func_doc) + + xrefs.clear() + + return '\n\n'.join(functions), '\n\n'.join(deprecated_functions) + + +def delete_lines_below(filename, tokenstr): + """Deletes all lines below the line containing `tokenstr`, the line itself, + and one line above it. + """ + lines = open(filename).readlines() + i = 0 + for i, line in enumerate(lines, 1): + if tokenstr in line: + break + i = max(0, i - 2) + with open(filename, 'wt') as fp: + fp.writelines(lines[0:i]) + +def gen_docs(config): + """Generate documentation. + + Doxygen is called and configured through stdin. + """ + p = subprocess.Popen(['doxygen', '-'], stdin=subprocess.PIPE) + p.communicate(config.format(input=src_dir, output=out_dir, + filter=filter_cmd).encode('utf8')) + if p.returncode: + sys.exit(p.returncode) + + sections = {} + intros = {} + sep = '=' * text_width + + base = os.path.join(out_dir, 'xml') + dom = minidom.parse(os.path.join(base, 'index.xml')) + + # generate docs for section intros + for compound in dom.getElementsByTagName('compound'): + if compound.getAttribute('kind') != 'group': + continue + + groupname = get_text(find_first(compound, 'name')) + groupxml = os.path.join(base, '%s.xml' % compound.getAttribute('refid')) + + desc = find_first(minidom.parse(groupxml), 'detaileddescription') + if desc: + doc = parse_parblock(desc) + if doc: + intros[groupname] = doc + + for compound in dom.getElementsByTagName('compound'): + if compound.getAttribute('kind') != 'file': + continue + + filename = get_text(find_first(compound, 'name')) + if filename.endswith('.c'): + functions, deprecated = parse_source_xml( + os.path.join(base, '%s.xml' % compound.getAttribute('refid'))) + + if not functions and not deprecated: + continue + + if functions or deprecated: + name = os.path.splitext(os.path.basename(filename))[0] + if name == 'ui': + name = name.upper() + else: + name = name.title() + + doc = '' + + intro = intros.get('api-%s' % name.lower()) + if intro: + doc += '\n\n' + intro + + if functions: + doc += '\n\n' + functions + + if INCLUDE_DEPRECATED and deprecated: + doc += '\n\n\nDeprecated %s Functions: ~\n\n' % name + doc += deprecated + + if doc: + filename = os.path.basename(filename) + name = section_name.get(filename, name) + title = '%s Functions' % name + helptag = '*api-%s*' % name.lower() + sections[filename] = (title, helptag, doc) + + if not sections: + return + + docs = '' + + i = 0 + for filename in section_order: + if filename not in sections: + continue + title, helptag, section_doc = sections.pop(filename) + + i += 1 + docs += sep + docs += '\n%s%s' % (title, helptag.rjust(text_width - len(title))) + docs += section_doc + docs += '\n\n\n' + + if sections: + # In case new API sources are added without updating the order dict. + for title, helptag, section_doc in sections.values(): + i += 1 + docs += sep + docs += '\n%s%s' % (title, helptag.rjust(text_width - len(title))) + docs += section_doc + docs += '\n\n\n' + + docs = docs.rstrip() + '\n\n' + docs += ' vim:tw=78:ts=8:ft=help:norl:\n' + + doc_file = os.path.join(base_dir, 'runtime/doc', doc_filename) + delete_lines_below(doc_file, section_start_token) + with open(doc_file, 'ab') as fp: + fp.write(docs.encode('utf8')) + shutil.rmtree(out_dir) + + +def filter_source(filename): + """Filters the source to fix macros that confuse Doxygen.""" + with open(filename, 'rt') as fp: + print(re.sub(r'^(ArrayOf|DictionaryOf)(\(.*?\))', + lambda m: m.group(1)+'_'.join( + re.split(r'[^\w]+', m.group(2))), + fp.read(), flags=re.M)) + + +# Doxygen Config {{{ +Doxyfile = ''' +OUTPUT_DIRECTORY = {output} +INPUT = {input} +INPUT_ENCODING = UTF-8 +FILE_PATTERNS = *.h *.c +RECURSIVE = YES +INPUT_FILTER = "{filter}" +EXCLUDE = +EXCLUDE_SYMLINKS = NO +EXCLUDE_PATTERNS = */private/* +EXCLUDE_SYMBOLS = + +GENERATE_HTML = NO +GENERATE_DOCSET = NO +GENERATE_HTMLHELP = NO +GENERATE_QHP = NO +GENERATE_TREEVIEW = NO +GENERATE_LATEX = NO +GENERATE_RTF = NO +GENERATE_MAN = NO +GENERATE_DOCBOOK = NO +GENERATE_AUTOGEN_DEF = NO + +GENERATE_XML = YES +XML_OUTPUT = xml +XML_PROGRAMLISTING = NO + +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = NO +MARKDOWN_SUPPORT = YES +''' +# }}} + +if __name__ == "__main__": + if len(sys.argv) > 1: + filter_source(sys.argv[1]) + else: + gen_docs(Doxyfile) + +# vim: set ft=python ts=4 sw=4 tw=79 et fdm=marker : -- cgit