Skip to content

Commit a2bfe75

Browse files
committed
Added support for generating sections of documentation
Added python tests Added Python tests Fixed rebase conflicts
1 parent e932ba9 commit a2bfe75

File tree

7 files changed

+293
-11
lines changed

7 files changed

+293
-11
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,23 @@ local opts = { noremap = true, silent = true }
105105
vim.api.nvim_set_keymap("n", "<Leader>nc", ":lua require('neogen').generate({ type = 'class' })<CR>", opts)
106106
```
107107

108+
If you'd like to generate only part of a docstring, add `sections`.
109+
110+
```lua
111+
require('neogen').generate({
112+
type = "func" -- the annotation type to generate. Currently supported: func, class, type, file
113+
sections = {"parameter", "return"}
114+
})
115+
```
116+
117+
Example sections:
118+
```
119+
parameter
120+
return
121+
throw
122+
yield
123+
```
124+
108125
### Snippet support
109126

110127
We added snippet support, and we provide defaults for some snippet engines.

doc/neogen.txt

+5-2
Original file line numberDiff line numberDiff line change
@@ -182,12 +182,15 @@ Feel free to submit a PR, I will be happy to help you !
182182
We use semantic versioning ! (https://semver.org)
183183
Here is the current Neogen version:
184184
>lua
185-
neogen.version = "2.19.4"
185+
186+
neogen.version = "2.20.0"
186187
<
187188
# Changelog~
188189

189190
Note: We will only document `major` and `minor` versions, not `patch` ones. (only X and Y in X.Y.z)
190191

192+
## 2.20.0~
193+
- Added `sections` support for docstring generation (#185)
191194
## 2.19.0~
192195
- Add support for julia (`julia`) ! (#185)
193196
## 2.18.0~
@@ -464,4 +467,4 @@ If not specified, will use this line for all types.
464467
{required} `(string)` If specified, is used in if the first field of the table is a `table` (example above)
465468

466469

467-
vim:tw=78:ts=8:noet:ft=help:norl:
470+
vim:tw=78:ts=8:noet:ft=help:norl:

lua/neogen/configurations/python.lua

+1-1
Original file line numberDiff line numberDiff line change
@@ -460,8 +460,8 @@ return {
460460
},
461461
},
462462
},
463+
locator = require("neogen.locators.python"),
463464
-- Use default granulator and generator
464-
locator = nil,
465465
granulator = nil,
466466
generator = nil,
467467
template = template

lua/neogen/generator.lua

+120-4
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,13 @@ end
152152
---@param template table a template from the configuration
153153
---@param required_type string
154154
---@param annotation_convention string
155+
---@param sections string[] | string the parts of a docstring to create
155156
---@return table { line, content }, with line being the line to append the content
156-
local function generate_content(parent, data, template, required_type, annotation_convention)
157+
local function generate_content(parent, data, template, required_type, annotation_convention, partial)
158+
if partial == nil then
159+
partial = false
160+
end
161+
157162
local row, col = get_place_pos(parent, template.position, template.append, required_type)
158163

159164
local commentstring = vim.trim(vim.bo.commentstring:format(""))
@@ -188,7 +193,7 @@ local function generate_content(parent, data, template, required_type, annotatio
188193
end
189194

190195
local ins_type = type(inserted_type)
191-
if ins_type == "nil" then
196+
if not partial and ins_type == "nil" then
192197
local no_data = vim.tbl_isempty(data)
193198
if opts.no_results then
194199
if no_data then
@@ -231,11 +236,110 @@ local function generate_content(parent, data, template, required_type, annotatio
231236
end
232237
end
233238

239+
if partial then
240+
local index = 1
241+
242+
while result[index] == "" do
243+
table.remove(result, index)
244+
end
245+
end
246+
234247
return row, result, default_text
235248
end
236249

250+
--- Interpret all `sections` into Neogen-compatible section names.
251+
--- Each section name is a partial match. e.g. "parameter" will match
252+
--- "has_parameter" and "parameters".
253+
---@param sections string[] | string A user's desired parts of a docstring to create.
254+
---@return string[] # The resolved section names.
255+
---
256+
local function expand_sections(sections)
257+
local function has_match(expression, options)
258+
for _, option in ipairs(options) do
259+
if option:match(expression) then
260+
return true
261+
end
262+
end
263+
264+
return false
265+
end
266+
267+
local i = require("neogen.types.template").item
268+
269+
if type(sections) == "string" then
270+
sections = {sections}
271+
end
272+
273+
local has_parameter = false
274+
local parameters = {
275+
i.ArbitraryArgs,
276+
i.HasParameter,
277+
i.Kwargs,
278+
i.Parameter,
279+
i.Tparam,
280+
i.Vararg,
281+
}
282+
283+
local has_return = false
284+
local returns = {
285+
i.HasReturn,
286+
i.Return,
287+
i.ReturnAnonym,
288+
i.ReturnTypeHint,
289+
}
290+
291+
local has_throw = false
292+
local throws = { i.HasThrow, i.Throw }
293+
294+
local has_yield = false
295+
local yields = { i.HasYield, i.Yield }
296+
297+
local output = {}
298+
299+
for _, section in ipairs(sections) do
300+
if not has_parameter and has_match(section, parameters) then
301+
vim.list_extend(output, parameters)
302+
has_parameter = true
303+
end
304+
305+
if not has_return and has_match(section, returns) then
306+
vim.list_extend(output, returns)
307+
has_return = true
308+
end
309+
310+
if not has_throw and has_match(section, throws) then
311+
vim.list_extend(output, throws)
312+
has_throw = true
313+
end
314+
315+
if not has_yield and has_match(section, yields) then
316+
vim.list_extend(output, yields)
317+
has_yield = true
318+
end
319+
end
320+
321+
return output
322+
end
323+
324+
--- Remove `data` that is not present in `sections`.
325+
---@param data table the data from configurations[lang].data
326+
---@param sections string[] Any part of `data` not found in `sections` will be omitted.
327+
---@return table # The filtered output of `data`.
328+
local function filter_by_sections(data, sections)
329+
local output = {}
330+
331+
for key, value in pairs(data) do
332+
if vim.tbl_contains(sections, key) then
333+
output[key] = value
334+
end
335+
end
336+
337+
return output
338+
end
339+
340+
237341
return setmetatable({}, {
238-
__call = function(_, filetype, node_type, return_snippet, annotation_convention)
342+
__call = function(_, filetype, node_type, return_snippet, annotation_convention, sections)
239343
if filetype == "" then
240344
notify("No filetype detected", vim.log.levels.WARN)
241345
return
@@ -279,9 +383,21 @@ return setmetatable({}, {
279383

280384
local data = granulator(parent_node, language.data[node_type])
281385

386+
local partial = false
387+
388+
if sections ~= nil then
389+
partial = true
390+
sections = expand_sections(sections)
391+
data = filter_by_sections(data, sections)
392+
end
393+
282394
-- Will try to generate the documentation from a template and the data found from the granulator
283395
local row, template_content, default_text =
284-
generate_content(parent_node, data, template, node_type, annotation_convention[filetype])
396+
generate_content(parent_node, data, template, node_type, annotation_convention[filetype], partial)
397+
398+
if partial then
399+
row = vim.api.nvim_win_get_cursor(0)[1]
400+
end
285401

286402
local content = {}
287403
local marks_pos = {}

lua/neogen/init.lua

+2-2
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ neogen.generate = function(opts)
167167
end
168168

169169
opts = opts or {}
170-
return require("neogen.generator")(vim.bo.filetype, opts.type, opts.return_snippet, opts.annotation_convention)
170+
return require("neogen.generator")(vim.bo.filetype, opts.type, opts.return_snippet, opts.annotation_convention, opts.sections)
171171
end
172172

173173
-- Expose more API ============================================================
@@ -309,7 +309,7 @@ end
309309
--- with multiple annotation conventions.
310310
---@tag neogen-changelog
311311
---@toc_entry Changes in neogen plugin
312-
neogen.version = "2.19.4"
312+
neogen.version = "2.20.0"
313313
--minidoc_afterlines_end
314314

315315
return neogen

lua/neogen/locators/python.lua

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---@param node_info Neogen.node_info
2+
---@param nodes_to_match TSNode[]
3+
---@return TSNode?
4+
return function(node_info, nodes_to_match)
5+
local current = node_info.current
6+
local parents = { "class_definition", "function_definition", "module" }
7+
8+
while current do
9+
if vim.tbl_contains(parents, current:type()) then
10+
return current
11+
end
12+
13+
current = current:parent()
14+
end
15+
16+
return nil
17+
end

tests/neogen/python_google_spec.lua

+131-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44

55
local specs = require('tests.utils.specs')
66

7-
local function make_google_docstrings(source)
8-
return specs.make_docstring(source, 'python', { annotation_convention = { python = 'google_docstrings' } })
7+
--- Parse `source` and expand all docstrings with Neogen.
8+
---@param source string Pseudo-source-code to call. It contains `"|cursor|"` which is the expected user position.
9+
---@param sections (string[] | string)? Parts of the docstring to create. e.g. `"parameters"`.
10+
---@return string # The real, Neogen-generated source code result.
11+
local function make_google_docstrings(source, sections)
12+
return specs.make_docstring(source, 'python', { annotation_convention = { python = 'google_docstrings' }, sections=sections })
913
end
1014

1115
describe("python: google_docstrings", function()
@@ -863,4 +867,129 @@ describe("python: google_docstrings", function()
863867
assert.equal(expected, result)
864868
end)
865869
end)
870+
871+
describe("sections", function()
872+
it("works even with no sections are given", function()
873+
local source = [[
874+
def foo(thing):|cursor|
875+
if thing:
876+
yield 10
877+
yield 20
878+
yield 30
879+
else:
880+
yield 0
881+
882+
for _ in range(10):
883+
yield
884+
]]
885+
886+
local expected = [[
887+
def foo(thing):
888+
"""[TODO:description]
889+
890+
Args:
891+
thing ([TODO:parameter]): [TODO:description]
892+
893+
Yields:
894+
[TODO:description]
895+
"""
896+
if thing:
897+
yield 10
898+
yield 20
899+
yield 30
900+
else:
901+
yield 0
902+
903+
for _ in range(10):
904+
yield
905+
]]
906+
907+
local result = make_google_docstrings(source)
908+
909+
assert.equal(expected, result)
910+
end)
911+
912+
it("works with 1 section", function()
913+
local source = [[
914+
def foo(thing):
915+
"""An existing docstring.
916+
|cursor|
917+
"""
918+
if thing:
919+
yield 10
920+
yield 20
921+
yield 30
922+
else:
923+
yield 0
924+
925+
for _ in range(10):
926+
yield
927+
]]
928+
929+
local expected = [[
930+
def foo(thing):
931+
"""An existing docstring.
932+
933+
Yields:
934+
[TODO:description]
935+
"""
936+
if thing:
937+
yield 10
938+
yield 20
939+
yield 30
940+
else:
941+
yield 0
942+
943+
for _ in range(10):
944+
yield
945+
]]
946+
947+
local result = make_google_docstrings(source, {"yield"})
948+
949+
assert.equal(expected, result)
950+
end)
951+
952+
it("works with 2+ sections", function()
953+
local source = [[
954+
def foo(thing):
955+
"""An existing docstring.
956+
|cursor|
957+
"""
958+
if thing:
959+
yield 10
960+
yield 20
961+
yield 30
962+
else:
963+
yield 0
964+
965+
for _ in range(10):
966+
yield
967+
]]
968+
969+
local expected = [[
970+
def foo(thing):
971+
"""An existing docstring.
972+
973+
Args:
974+
thing ([TODO:parameter]): [TODO:description]
975+
976+
Yields:
977+
[TODO:description]
978+
"""
979+
if thing:
980+
yield 10
981+
yield 20
982+
yield 30
983+
else:
984+
yield 0
985+
986+
for _ in range(10):
987+
yield
988+
]]
989+
990+
local result = make_google_docstrings(source, {"parameters", "yield"})
991+
992+
assert.equal(expected, result)
993+
end)
994+
end)
866995
end)

0 commit comments

Comments
 (0)