[mirror] Make your go dev experience better github.com/olexsmir/gopher.nvim
neovim golang

feat(comment): add support for: interface methods, struct fields, variables (#123)

* refactor(comment): dont use unnecessary function

* chore: quick way to open vim in dev mode

* feat(comment): add comment on on interface method

* feat(comment): add comment on a struct field

* feat(comment): add comment on a variable

* docs: add note about the generate function

* docs: gopher.TsResult

* fix(utils): handle case when indentation is wrong

authored by olexsmir.xyz and committed by GitHub 1e7af1b2 295e21e6

+2
Taskfile.yml
··· 21 21 -u ./scripts/minimal_init.lua \ 22 22 -c "lua MiniTest.run()" \ 23 23 -c ":qa!" 24 + nvim: 25 + cmd: nvim --clean -u "./scripts/minimal_init.lua" {{ .CLI_ARGS }} 24 26 25 27 docgen: 26 28 desc: generate vimhelp
+13 -1
lua/gopher/_utils/init.lua
··· 3 3 local utils = {} 4 4 5 5 ---@param msg string 6 - ---@param lvl? number by default `vim.log.levels.INFO` 6 + ---@param lvl? integer by default `vim.log.levels.INFO` 7 7 function utils.notify(msg, lvl) 8 8 lvl = lvl or vim.log.levels.INFO 9 9 vim.notify(msg, lvl, { ··· 36 36 function utils.trimend(s) 37 37 local r, _ = string.gsub(s, "%s+$", "") 38 38 return r 39 + end 40 + 41 + -- Since indentation can be spaces or tabs, that's my hack around it 42 + ---@param line string 43 + ---@param indent integer 44 + ---@return string 45 + function utils.indent(line, indent) 46 + local char = string.sub(line, 1, 1) 47 + if char ~= " " and char ~= "\t" then 48 + char = " " 49 + end 50 + return string.rep(char, indent) 39 51 end 40 52 41 53 return utils
+40 -16
lua/gopher/_utils/ts.lua
··· 11 11 right: (expression_list (composite_literal 12 12 type: (struct_type))))] 13 13 ]], 14 + struct_field = [[ 15 + (field_declaration name: (field_identifier) @_name) 16 + ]], 14 17 func = [[ 15 18 [(function_declaration name: (identifier) @_name) 16 - (method_declaration name: (field_identifier) @_name)] 19 + (method_declaration name: (field_identifier) @_name) 20 + (method_elem name: (field_identifier) @_name)] 17 21 ]], 18 22 package = [[ 19 23 (package_identifier) @_name ··· 23 27 name: (type_identifier) @_name 24 28 type: (interface_type)) 25 29 ]], 30 + var = [[ 31 + [(var_declaration (var_spec name: (identifier) @_name)) 32 + (short_var_declaration 33 + left: (expression_list (identifier) @_name @_var))] 34 + ]], 26 35 } 27 36 28 37 ---@param parent_type string[] 29 38 ---@param node TSNode 30 39 ---@return TSNode? 31 - local function get_parrent_node(parent_type, node) 40 + local function get_parent_node(parent_type, node) 32 41 ---@type TSNode? 33 42 local current = node 34 43 while current do ··· 64 73 end 65 74 66 75 ---@class gopher.TsResult 67 - ---@field name string 68 - ---@field start integer 69 - ---@field end_ integer 70 - ---@field is_varstruct boolean 76 + ---@field name string Name of the struct, function, etc 77 + ---@field start integer Line number where the declaration starts 78 + ---@field end_ integer Line number where the declaration ends 79 + ---@field indent integer Number of spaces/tabs in the current cursor line 80 + ---@field is_varstruct boolean Is struct declared as `var S struct{}` or `s := struct{}{}` 71 81 72 82 ---@param bufnr integer 73 83 ---@param parent_type string[] ··· 78 88 error "No treesitter parser found for go" 79 89 end 80 90 81 - local node = vim.treesitter.get_node { 82 - bufnr = bufnr, 83 - } 91 + local node = vim.treesitter.get_node { bufnr = bufnr } 84 92 if not node then 85 - error "No nodes found under cursor" 93 + error "No nodes found under the cursor" 86 94 end 87 95 88 - local parent_node = get_parrent_node(parent_type, node) 96 + local parent_node = get_parent_node(parent_type, node) 89 97 if not parent_node then 90 - error "No parent node found under cursor" 98 + error "No parent node found under the cursor" 91 99 end 92 100 93 101 local q = vim.treesitter.query.parse("go", query) 94 102 local res = get_captures(q, parent_node, bufnr) 95 103 assert(res.name ~= nil, "No capture name found") 96 104 97 - local start_row, _, end_row, _ = parent_node:range() 105 + local start_row, start_col, end_row, _ = parent_node:range() 106 + res["indent"] = start_col 98 107 res["start"] = start_row + 1 99 108 res["end_"] = end_row + 1 100 109 ··· 104 113 ---@param bufnr integer 105 114 function ts.get_struct_under_cursor(bufnr) 106 115 --- should be both type_spec and type_declaration 107 - --- because in cases like `type ( T struct{}, U strict{} )` 108 - --- i will be choosing always last struct in the list 116 + --- because in cases like `type ( T struct{}, U struct{} )` 109 117 --- 110 118 --- var_declaration is for cases like `var x struct{}` 111 119 --- short_var_declaration is for cases like `x := struct{}{}` 120 + --- 121 + --- it always chooses last struct type in the list 112 122 return do_stuff(bufnr, { 113 123 "type_spec", 114 124 "type_declaration", ··· 118 128 end 119 129 120 130 ---@param bufnr integer 131 + function ts.get_struct_field_under_cursor(bufnr) 132 + return do_stuff(bufnr, { "field_declaration" }, queries.struct_field) 133 + end 134 + 135 + ---@param bufnr integer 121 136 function ts.get_func_under_cursor(bufnr) 122 137 --- since this handles both and funcs and methods we should check for both parent nodes 123 - return do_stuff(bufnr, { "function_declaration", "method_declaration" }, queries.func) 138 + return do_stuff(bufnr, { 139 + "method_elem", 140 + "function_declaration", 141 + "method_declaration", 142 + }, queries.func) 124 143 end 125 144 126 145 ---@param bufnr integer ··· 131 150 ---@param bufnr integer 132 151 function ts.get_interface_under_cursor(bufnr) 133 152 return do_stuff(bufnr, { "type_declaration" }, queries.interface) 153 + end 154 + 155 + ---@param bufnr integer 156 + function ts.get_variable_under_cursor(bufnr) 157 + return do_stuff(bufnr, { "var_declaration", "short_var_declaration" }, queries.var) 134 158 end 135 159 136 160 return ts
+28 -16
lua/gopher/comment.lua
··· 7 7 8 8 local ts = require "gopher._utils.ts" 9 9 local log = require "gopher._utils.log" 10 + local u = require "gopher._utils" 10 11 local comment = {} 11 12 12 - ---@param name string 13 - ---@return string 14 - ---@dochide 15 - local function template(name) 16 - return "// " .. name .. " " 17 - end 18 - 13 + --- NOTE: The order of functions executed inside this function is IMPORTANT. 14 + --- This function is extremely fragile; run tests after making any changes. 15 + --- 19 16 ---@param bufnr integer 17 + ---@param line string 20 18 ---@return string 21 19 ---@dochide 22 - local function generate(bufnr) 20 + local function generate(bufnr, line) 21 + local sf_ok, sf_res = pcall(ts.get_struct_field_under_cursor, bufnr) 22 + if sf_ok then 23 + return u.indent(line, sf_res.indent) .. "// " .. sf_res.name .. " " 24 + end 25 + 23 26 local s_ok, s_res = pcall(ts.get_struct_under_cursor, bufnr) 24 27 if s_ok then 25 - return template(s_res.name) 28 + return u.indent(line, s_res.indent) .. "// " .. s_res.name .. " " 29 + end 30 + 31 + local v_ok, v_res = pcall(ts.get_variable_under_cursor, bufnr) 32 + if v_ok then 33 + return u.indent(line, v_res.indent) .. "// " .. v_res.name .. " " 26 34 end 27 35 28 36 local f_ok, f_res = pcall(ts.get_func_under_cursor, bufnr) 29 37 if f_ok then 30 - return template(f_res.name) 38 + return u.indent(line, f_res.indent) .. "// " .. f_res.name .. " " 31 39 end 32 40 33 41 local i_ok, i_res = pcall(ts.get_interface_under_cursor, bufnr) 34 42 if i_ok then 35 - return template(i_res.name) 43 + return u.indent(line, i_res.indent) .. "// " .. i_res.name .. " " 36 44 end 37 45 38 46 local p_ok, p_res = pcall(ts.get_package_under_cursor, bufnr) ··· 45 53 46 54 function comment.comment() 47 55 local bufnr = vim.api.nvim_get_current_buf() 48 - local cmt = generate(bufnr) 49 - log.debug("generated comment: " .. cmt) 56 + local lnum = vim.fn.getcurpos()[2] 57 + local line = vim.fn.getline(lnum) 58 + local cmt = generate(bufnr, line) 59 + log.debug("generated comment:", { 60 + comment = cmt, 61 + line = line, 62 + }) 50 63 51 - local pos = vim.fn.getcurpos()[2] 52 - vim.fn.append(pos - 1, cmt) 53 - vim.fn.setpos(".", { 0, pos, #cmt }) 64 + vim.fn.append(lnum - 1, cmt) 65 + vim.fn.setpos(".", { bufnr, lnum, #cmt }) 54 66 vim.cmd "startinsert!" 55 67 end 56 68
+6
spec/fixtures/comment/interface_many_method_input.go
··· 1 + package main 2 + 3 + type Testinger interface { 4 + Get(id string) int 5 + Set(id string, val int) 6 + }
+7
spec/fixtures/comment/interface_many_method_output.go
··· 1 + package main 2 + 3 + type Testinger interface { 4 + Get(id string) int 5 + // Set 6 + Set(id string, val int) 7 + }
+5
spec/fixtures/comment/interface_method_input.go
··· 1 + package main 2 + 3 + type Testinger interface { 4 + Method(input string) error 5 + }
+6
spec/fixtures/comment/interface_method_output.go
··· 1 + package main 2 + 3 + type Testinger interface { 4 + // Method 5 + Method(input string) error 6 + }
+18
spec/fixtures/comment/many_structs_fields_input.go
··· 1 + package main 2 + 3 + type ( 4 + TestOne struct { 5 + Asdf string 6 + ID int 7 + } 8 + 9 + TestTwo struct { 10 + Fesa int 11 + A bool 12 + } 13 + 14 + TestThree struct { 15 + Asufj int 16 + Fs string 17 + } 18 + )
+19
spec/fixtures/comment/many_structs_fields_output.go
··· 1 + package main 2 + 3 + type ( 4 + TestOne struct { 5 + Asdf string 6 + ID int 7 + } 8 + 9 + TestTwo struct { 10 + // Fesa 11 + Fesa int 12 + A bool 13 + } 14 + 15 + TestThree struct { 16 + Asufj int 17 + Fs string 18 + } 19 + )
+7
spec/fixtures/comment/struct_fields_input.go
··· 1 + package main 2 + 3 + type CommentStruct struct { 4 + Name string 5 + Address string 6 + Aliases []string 7 + }
+8
spec/fixtures/comment/struct_fields_output.go
··· 1 + package main 2 + 3 + type CommentStruct struct { 4 + Name string 5 + // Address 6 + Address string 7 + Aliases []string 8 + }
+5
spec/fixtures/comment/svar_input.go
··· 1 + package main 2 + 3 + func varTest() { 4 + s := "something" 5 + }
+6
spec/fixtures/comment/svar_output.go
··· 1 + package main 2 + 3 + func varTest() { 4 + // s 5 + s := "something" 6 + }
+5
spec/fixtures/comment/var_input.go
··· 1 + package main 2 + 3 + func test() { 4 + var imAVar string 5 + }
+6
spec/fixtures/comment/var_output.go
··· 1 + package main 2 + 3 + func test() { 4 + // imAVar 5 + var imAVar string 6 + }
+8
spec/fixtures/comment/var_struct_fields_input.go
··· 1 + package main 2 + 3 + func main() { 4 + var s struct { 5 + API string 6 + Key string 7 + } 8 + }
+9
spec/fixtures/comment/var_struct_fields_output.go
··· 1 + package main 2 + 3 + func main() { 4 + var s struct { 5 + API string 6 + // Key 7 + Key string 8 + } 9 + }
+28
spec/integration/comment_test.lua
··· 18 18 do_the_test("struct", { 4, 1 }) 19 19 end 20 20 21 + comment["should add a comment on struct field"] = function() 22 + do_the_test("struct_fields", { 5, 8 }) 23 + end 24 + 25 + comment["should add a comment on var struct field"] = function() 26 + do_the_test("var_struct_fields", { 6, 4 }) 27 + end 28 + 29 + comment["should add a comment on one field of many structs"] = function() 30 + do_the_test("many_structs_fields", { 10, 4 }) 31 + end 32 + 21 33 comment["should add comment to function"] = function() 22 34 do_the_test("func", { 3, 1 }) 23 35 end ··· 28 40 29 41 comment["should add comment to interface"] = function() 30 42 do_the_test("interface", { 3, 6 }) 43 + end 44 + 45 + comment["should add comment on interface method"] = function() 46 + do_the_test("interface_method", { 4, 2 }) 47 + end 48 + 49 + comment["should add a comment on interface with many method"] = function() 50 + do_the_test("interface_many_method", { 5, 2 }) 51 + end 52 + 53 + comment["should add a comment on a var"] = function() 54 + do_the_test("var", { 4, 2 }) 55 + end 56 + 57 + comment["should add a comment on a short declared var"] = function() 58 + do_the_test("svar", { 4, 8 }) 31 59 end 32 60 33 61 comment["otherwise should add // above cursor"] = function()
+24
spec/unit/utils_test.lua
··· 22 22 t.eq(u.trimend " hi ", " hi") 23 23 end 24 24 25 + utils["should add .indent() spaces"] = function() 26 + local u = require "gopher._utils" 27 + local line = " func Test() error {" 28 + local indent = 4 29 + 30 + t.eq(" ", u.indent(line, indent)) 31 + end 32 + 33 + utils["should add .indent() a tab"] = function() 34 + local u = require "gopher._utils" 35 + local line = "\tfunc Test() error {" 36 + local indent = 1 37 + 38 + t.eq("\t", u.indent(line, indent)) 39 + end 40 + 41 + utils["should add .indent() 2 tabs"] = function() 42 + local u = require "gopher._utils" 43 + local line = "\t\tfunc Test() error {" 44 + local indent = 2 45 + 46 + t.eq("\t\t", u.indent(line, indent)) 47 + end 48 + 25 49 return T