Merge pull request #106 from olexsmir/develop

This commit is contained in:
Smirnov Oleksandr 2025-03-23 23:08:09 +02:00 committed by GitHub
commit 847c79ab76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
81 changed files with 1588 additions and 1072 deletions

4
.envrc Normal file
View file

@ -0,0 +1,4 @@
dotenv
# GOPHER_DIR - needed only for tests, to find the root of the project
env_vars_required GOPHER_DIR

View file

@ -1,12 +1,18 @@
name: linters
on: [push, pull_request]
on:
push:
branches:
- main
- develop
pull_request:
jobs:
linters:
name: linters
name: Lua
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: JohnnyMorganz/stylua-action@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
@ -17,3 +23,36 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: .
docs:
name: Docs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Task
uses: arduino/setup-task@v1
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install NeoVim
uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: stable
- name: Cache .tests
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/.tests
key: ${{ runner.os }}-tests-${{ hashFiles('${{ github.workspace }}/.tests') }}
- name: Generate docs
run: task docgen
- name: Diff
run: |
git diff doc
exit $(git status --porcelain doc | wc -l | tr -d " ")

View file

@ -1,25 +1,20 @@
name: tests
on: [push, pull_request]
on:
push:
branches:
- main
- develop
pull_request:
jobs:
tests:
strategy:
matrix:
os: [ubuntu-latest]
nvim_version:
version:
- stable
- nightly
- v0.7.0
- v0.7.2
- v0.8.0
- v0.8.1
- v0.8.2
- v0.8.3
- v0.9.0
- v0.9.1
- v0.9.2
- v0.9.4
- v0.9.5
- v0.10.0
runs-on: ${{ matrix.os }}
steps:
- name: Install Task
@ -28,18 +23,38 @@ jobs:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: "1.24.0"
check-latest: false
- name: Install Neovim
- name: Install NeoVim
uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: ${{ matrix.version }}
- uses: actions/checkout@v4
- name: Cache .tests
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/.tests
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-tests-${{ hashFiles('${{ github.workspace }}/.tests') }}
- name: Install Go bins
run: |
mkdir -p /tmp/nvim
wget -q https://github.com/neovim/neovim/releases/download/${{ matrix.nvim_version }}/nvim.appimage -O /tmp/nvim/nvim.appimage
cd /tmp/nvim
chmod a+x ./nvim.appimage
./nvim.appimage --appimage-extract
echo "/tmp/nvim/squashfs-root/usr/bin/" >> $GITHUB_PATH
# TODO: install with :GoInstallDeps
go install github.com/fatih/gomodifytags@latest
go install github.com/josharian/impl@latest
go install github.com/cweill/gotests/...@latest
go install github.com/koron/iferr@latest
- name: Run Tests
run: |
nvim --version
task test
task tests

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/playground/
/.tests/
/.env

View file

@ -1,10 +0,0 @@
{
"diagnostics.globals": [
"describe",
"it",
"before_each",
"after_each",
"before_all",
"after_all"
]
}

View file

@ -18,14 +18,12 @@ You can install these with:
```bash
sudo pacman -S selene stylua
# or whatever is your package manager
# or way of installing pkgs
```
For formatting use this following commands, or setup your editor to integrate with selene/stylua:
```bash
task format
task format:check # will check if your code formatted
task lint
task stylua
task lint # lintering and format chewing
```
### Documentation
@ -39,17 +37,15 @@ task docgen
```
### Commit messages
We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), please follow it.
### Testing
For testing this plugins uses [plenary.nvim](https://github.com/nvim-lua/plenary.nvim).
All tests live in [/spec](https://github.com/olexsmir/gopher.nvim/tree/main/spec) dir.
For testing this plugins uses [mini.test](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-test.md).
All tests live in [/spec](./spec) dir.
You can run tests with:
```bash
task test
# also there are some aliases for that
task tests
task spec
```

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Oleksandr Smirnov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -10,21 +10,19 @@ It's **NOT** an LSP tool, the main goal of this plugin is to add go tooling supp
## Install (using [lazy.nvim](https://github.com/folke/lazy.nvim))
Pre-dependency:
Requirements:
- [Go](https://github.com/golang/go)
- `go` treesitter parser, install by `:TSInstall go`
- **Neovim 0.10** or later
- Treesitter `go` parser(`:TSInstall go`)
- [Go](https://github.com/golang/go) installed (tested on 1.23)
```lua
{
"olexsmir/gopher.nvim",
ft = "go",
-- branch = "develop", -- if you want develop branch
-- keep in mind, it might break everything
-- branch = "develop"
dependencies = {
"nvim-lua/plenary.nvim",
"nvim-treesitter/nvim-treesitter",
"mfussenegger/nvim-dap", -- (optional) only if you use `gopher.dap`
},
-- (optional) will update plugin's deps on every update
build = function()
@ -35,23 +33,28 @@ Pre-dependency:
}
```
## Configuratoin
## Configuration
> [!IMPORTANT]
>
> If you need more info look `:h gopher.nvim`
**Take a look at default options**
**Take a look at default options (might be a bit outdated, look `:h gopher.nvim-config`)**
```lua
require("gopher").setup {
-- log level, you might consider using DEBUG or TRACE for debugging the plugin
log_level = vim.log.levels.INFO,
-- timeout for running internal commands
timeout = 2000,
commands = {
go = "go",
gomodifytags = "gomodifytags",
gotests = "gotests",
impl = "impl",
iferr = "iferr",
dlv = "dlv",
},
gotests = {
-- gotests doesn't have template named "default" so this plugin uses "default" to set the default template
@ -59,11 +62,16 @@ require("gopher").setup {
-- path to a directory containing custom test code templates
template_dir = nil,
-- switch table tests from using slice to map (with test name for the key)
-- works only with gotests installed from develop branch
named = false,
},
gotag = {
transform = "snakecase",
-- default tags to add to struct fields
default_tag = "json",
},
iferr = {
-- choose a custom error message
message = nil,
},
}
```
@ -87,7 +95,6 @@ require("gopher").setup {
- [impl](https://github.com/josharian/impl)
- [gotests](https://github.com/cweill/gotests)
- [iferr](https://github.com/koron/iferr)
- [dlv](github.com/go-delve/delve/cmd/dlv)
</details>
<details>
@ -215,20 +222,6 @@ require("gopher").setup {
```
</details>
<details>
<summary>
<b>Setup <a href="https://github.com/mfussenegger/nvim-dap">nvim-dap</a> for go in one line</b>
</summary>
THIS FEATURE WILL BE REMOVED IN `0.1.6`
note [nvim-dap](https://github.com/mfussenegger/nvim-dap) has to be installed
```lua
require("gopher.dap").setup()
```
</details>
## Contributing
PRs are always welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md)
@ -236,5 +229,4 @@ PRs are always welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md)
## Thanks
- [go.nvim](https://github.com/ray-x/go.nvim)
- [nvim-dap-go](https://github.com/leoluz/nvim-dap-go)
- [iferr](https://github.com/koron/iferr)

View file

@ -1,49 +1,34 @@
version: "3"
tasks:
format:
desc: formats all lua files in repo
cmds:
- stylua .
lint:
desc: runs all linters
cmds:
- task: selene
- task: stylua:check
- stylua --check .
selene:
desc: runs lua linter(selene)
cmds:
- selene .
stylua:check:
desc: runs stylua in check mode
cmds:
- stylua --check .
stylua:
desc: runs lua formatter
cmds:
- stylua .
test:
desc: runs all tests
aliases: [tests, spec]
tests:
desc: run all tests
cmds:
- |
nvim --headless \
nvim --clean --headless \
-u ./scripts/minimal_init.lua \
-c "PlenaryBustedDirectory spec \
{minimal_init='./scripts/minimal_init.lua' \
,sequential=true}" \
-c ":qa!"
-c "lua MiniTest.run()"
docgen:
desc: generate vimhelp
cmds:
- |
nvim --noplugin \
--headless \
nvim --clean --headless \
-u "./scripts/minimal_init.lua" \
-c "luafile ./scripts/docgen.lua" \
-c ":qa!"

View file

@ -1,3 +0,0 @@
function! health#gopher#check()
lua require("gopher.health").check()
endfunction

View file

@ -1,61 +1,62 @@
*gopher.nvim*
*gopher.nvim* Enhance your golang experience
MIT License Copyright (c) 2025 Oleksandr Smirnov
==============================================================================
gopher.nvim is a minimalistic plugin for Go development in Neovim written in Lua.
It's not an LSP tool, the main goal of this plugin is add go tooling support in Neovim.
------------------------------------------------------------------------------
*gopher.nvim-table-of-contents*
Table of Contents
Setup....................................................|gopher.nvim-setup|
Install dependencies..............................|gopher.nvim-install-deps|
Configuration...........................................|gopher.nvim-config|
Modifty struct tags................................|gopher.nvim-struct-tags|
Auto implementation of interface methods..................|gopher.nvim-impl|
Generating unit tests boilerplate......................|gopher.nvim-gotests|
Iferr....................................................|gopher.nvim-iferr|
Generate comments.....................................|gopher.nvim-comments|
Setup `nvim-dap` for Go......................................|gopher.nvim-dap|
Setup ................................................ |gopher.nvim-setup()|
Install dependencies ............................ |gopher.nvim-dependencies|
Config ................................................ |gopher.nvim-config|
Commands ............................................ |gopher.nvim-commands|
Modify struct tags ............................... |gopher.nvim-struct-tags|
Auto implementation of interface methods ................ |gopher.nvim-impl|
Generating unit tests boilerplate .................... |gopher.nvim-gotests|
Iferr .................................................. |gopher.nvim-iferr|
Generate comments ................................... |gopher.nvim-comments|
------------------------------------------------------------------------------
*gopher.nvim-setup*
*gopher.nvim-setup()*
`gopher.setup`({user_config})
Setup function. This method simply merges default configs with opts table.
Setup function. This method simply merges default config with opts table.
You can read more about configuration at |gopher.nvim-config|
Calling this function is optional, if you ok with default settings. Look |gopher.nvim.config-defaults|
Calling this function is optional, if you ok with default settings.
See |gopher.nvim.config|
Usage ~
`require("gopher").setup {}` (replace `{}` with your `config` table)
>lua
require("gopher").setup {} -- use default config or replace {} with your own
<
Parameters ~
{user_config} gopher.Config
{user_config} `(gopher.Config)` See |gopher.nvim-config|
------------------------------------------------------------------------------
*gopher.nvim-install-deps*
*gopher.nvim-dependencies*
`gopher.install_deps`
Gopher.nvim implements most of its features using third-party tools.
To install these tools, you can run `:GoInstallDeps` command
or call `require("gopher").install_deps()` if you want ues lua api.
or call `require("gopher").install_deps()` if you want to use lua api.
By default dependencies will be installed asynchronously,
to install them synchronously pass `{sync = true}` as an argument.
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-config*
config it is the place where you can configure the plugin.
also this is optional is you're ok with default settings.
You can look at default options |gopher.nvim-config-defaults|
------------------------------------------------------------------------------
*gopher.nvim-config-defaults*
`default_config`
>lua
local default_config = {
--minidoc_replace_end
-- log level, you might consider using DEBUG or TRACE for degugging the plugin
-- log level, you might consider using DEBUG or TRACE for debugging the plugin
---@type number
log_level = vim.log.levels.INFO,
-- timeout for running internal commands
---@type number
timeout = 2000,
-- user specified paths to binaries
---@class gopher.ConfigCommand
commands = {
@ -64,7 +65,6 @@ You can look at default options |gopher.nvim-config-defaults|
gotests = "gotests",
impl = "impl",
iferr = "iferr",
dlv = "dlv",
},
---@class gopher.ConfigGotests
gotests = {
@ -74,13 +74,20 @@ You can look at default options |gopher.nvim-config-defaults|
---@type string|nil
template_dir = nil,
-- switch table tests from using slice to map (with test name for the key)
-- works only with gotests installed from develop branch
named = false,
},
---@class gopher.ConfigGoTag
gotag = {
---@type gopher.ConfigGoTagTransform
transform = "snakecase",
-- default tags to add to struct fields
default_tag = "json",
},
iferr = {
-- choose a custom error message
---@type string|nil
message = nil,
},
}
<
@ -88,18 +95,32 @@ Class ~
{gopher.Config}
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-commands*
If don't want to automatically register plugins' commands,
you can set `vim.g.gopher_register_commands` to `false`, before loading the plugin.
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-struct-tags*
struct-tags is utilizing the `gomodifytags` tool to add or remove tags to struct fields.
`struct_tags` is utilizing the `gomodifytags` tool to add or remove tags to struct fields.
Usage ~
- put your coursor on the struct
- run `:GoTagAdd json` to add json tags to struct fields
- run `:GoTagRm json` to remove json tags to struct fields
note: if you dont spesify the tag it will use `json` as default
How to add/remove tags to struct fields:
1. Place cursor on the struct
2. Run `:GoTagAdd json` to add json tags to struct fields
3. Run `:GoTagRm json` to remove json tags to struct fields
simple example:
To clear all tags from struct run: `:GoTagClear`
NOTE: if you dont specify the tag it will use `json` as default
Example:
>go
// before
type User struct {
@ -116,26 +137,32 @@ simple example:
}
<
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-impl*
impl is utilizing the `impl` tool to generate method stubs for interfaces.
Integration of `impl` tool to generate method stubs for interfaces.
Usage ~
1. Automatically implement an interface for a struct:
- Place your cursor on the struct where you want to implement the interface.
- Run `:GoImpl io.Reader`
- This will automatically determine the receiver and implement the `io.Reader` interface.
1. put your coursor on the struct on which you want implement the interface
and run `:GoImpl io.Reader`
which will automatically choose the reciver for the methods and
implement the `io.Reader` interface
2. same as previous but with custom receiver, so put your coursor on the struct
run `:GoImpl w io.Writer`
where `w` is the receiver and `io.Writer` is the interface
3. specift receiver, struct, and interface
there's no need to put your coursor on the struct if you specify all arguments
`:GoImpl r RequestReader io.Reader`
where `r` is the receiver, `RequestReader` is the struct and `io.Reader` is the interface
2. Specify a custom receiver:
- Place your cursor on the struct
- Run `:GoImpl w io.Writer`, where:
- `w` is the receiver.
- `io.Writer` is the interface to implement.
simple example:
3. Explicitly specify the receiver, struct, and interface:
- No need to place the cursor on the struct if all arguments are provided.
- Run `:GoImpl r RequestReader io.Reader`, where:
- `r` is the receiver.
- `RequestReader` is the struct.
- `io.Reader` is the interface to implement.
Example:
>go
type BytesReader struct{}
// ^ put your cursor here
@ -147,74 +174,47 @@ simple example:
}
<
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-gotests*
gotests is utilizing the `gotests` tool to generate unit tests boilerplate.
Usage ~
- generate unit test for spesisfic function/method
- to specift the function/method put your cursor on it
- run `:GoTestAdd`
- Generate unit test for specific function/method:
1. Place your cursor on the desired function/method.
2. Run `:GoTestAdd`
- generate unit tests for all functions/methods in current file
- Generate unit tests for *all* functions/methods in current file:
- run `:GoTestsAll`
- generate unit tests only for exported(public) functions/methods
- Generate unit tests *only* for *exported(public)* functions/methods:
- run `:GoTestsExp`
you can also specify the template to use for generating the tests. see |gopher.nvim-config|
more details about templates can be found at: https://github.com/cweill/gotests
You can also specify the template to use for generating the tests. See |gopher.nvim-config|
More details about templates can be found at: https://github.com/cweill/gotests
------------------------------------------------------------------------------
*gopher.nvim-gotests-named*
if you prefare using named tests, you can enable it in the config.
but you would need to install `gotests@develop` because stable version doesn't support this feature.
you can do it with:
>lua
-- simply run go get in your shell:
go install github.com/cweill/gotests/...@develop
-- if you want to install it within neovim, you can use one of this:
vim.fn.jobstart("go install github.com/cweill/gotests/...@develop")
-- or if you want to use mason:
require("mason-tool-installer").setup {
ensure_installed = {
{ "gotests", version = "develop" },
}
}
<
if you choose to install `gotests` within neovim, i recommend adding it to your `build` section in your |lazy.nvim|
If you prefer named tests, you can enable them in |gopher.nvim-config|.
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-iferr*
if you're using `iferr` tool, this module provides a way to automatically insert `if err != nil` check.
`iferr` provides a way to way to automatically insert `if err != nil` check.
If you want to change `-message` option of `iferr` tool, see |gopher.nvim-config|
Usage ~
execute `:GoIfErr` near any err variable to insert the check
Execute `:GoIfErr` near any `err` variable to insert the check
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-comments*
Usage ~
Execute `:GoCmt` to generate a comment for the current function/method/struct/etc on this line.
This module provides a way to generate comments for Go code.
==============================================================================
------------------------------------------------------------------------------
*gopher.nvim-dap*
This module sets up `nvim-dap` for Go.
Usage ~
just call `require("gopher.dap").setup()`, and you're good to go.
Set cursor on line with function/method/struct/etc and run `:GoCmt` to generate a comment.
vim:tw=78:ts=8:noet:ft=help:norl:

View file

@ -25,10 +25,10 @@ end
---@param subcmd string
---@param args string[]
---@return string[]|nil
---@return string
function gocmd.run(subcmd, args)
if #args == 0 then
error "please provice any arguments"
if #args == 0 and subcmd ~= "generate" then
error "please provide any arguments"
end
if subcmd == "get" then
@ -39,15 +39,13 @@ function gocmd.run(subcmd, args)
args = if_generate(args)
end
return r.sync(c.go, {
args = { subcmd, unpack(args) },
on_exit = function(data, status)
if status ~= 0 then
error("gocmd failed: " .. data)
local rs = r.sync { c.go, subcmd, unpack(args) }
if rs.code ~= 0 then
error("go " .. subcmd .. " failed: " .. rs.stderr)
end
u.notify(c.go .. " " .. subcmd .. " successful runned")
end,
})
u.notify(c.go .. " " .. subcmd .. " ran successful")
return rs.stdout
end
return gocmd

View file

@ -1,33 +0,0 @@
local h = vim.health or require "health"
local health = {}
health.start = h.start or h.report_start
health.ok = h.ok or h.report_ok
health.warn = h.warn or h.report_warn
health.error = h.error or h.report_error
health.info = h.info or h.report_info
---@param module string
---@return boolean
function health.is_lualib_found(module)
local is_found, _ = pcall(require, module)
return is_found
end
---@param bin string
---@return boolean
function health.is_binary_found(bin)
if vim.fn.executable(bin) == 1 then
return true
end
return false
end
---@param ft string
---@return boolean
function health.is_treesitter_parser_available(ft)
local ok, parser = pcall(vim.treesitter.get_parser, 0, ft)
return ok and parser ~= nil
end
return health

View file

@ -2,33 +2,40 @@ local c = require "gopher.config"
local log = require "gopher._utils.log"
local utils = {}
---@param msg string
---@param lvl number
function utils.deferred_notify(msg, lvl)
vim.defer_fn(function()
vim.notify(msg, lvl, {
title = c.___plugin_name,
})
log.debug(msg)
end, 0)
end
---@param msg string
---@param lvl? number
function utils.notify(msg, lvl)
lvl = lvl or vim.log.levels.INFO
vim.notify(msg, lvl, {
---@diagnostic disable-next-line:undefined-field
title = c.___plugin_name,
})
log.debug(msg)
end
-- safe require
---@param module string module name
function utils.sreq(module)
local ok, m = pcall(require, module)
assert(ok, string.format("gopher.nvim dependency error: %s not installed", module))
return m
---@param path string
---@return string
function utils.readfile_joined(path)
return table.concat(vim.fn.readfile(path), "\n")
end
---@param t string[]
---@return string[]
function utils.remove_empty_lines(t)
local res = {}
for _, line in ipairs(t) do
if line ~= "" then
table.insert(res, line)
end
end
return res
end
---@param s string
---@return string
function utils.trimend(s)
local r, _ = string.gsub(s, "%s+$", "")
return r
end
return utils

View file

@ -18,6 +18,7 @@ local c = require "gopher.config"
local config = {
-- Name of the plugin. Prepended to log messages
---@diagnostic disable-next-line:undefined-field
name = c.___plugin_name,
-- Should print the output to neovim while running
@ -91,7 +92,7 @@ do
local log_at_level = function(level_config, message_maker, ...)
-- Return early if we're below the current_log_level
--
-- the log level source get from config directly because otherwise it doesnt work
-- the log level source get from config directly because otherwise it doesn't work
if level_config.level < c.log_level then
return
end

View file

@ -0,0 +1,39 @@
local c = require "gopher.config"
local runner = {}
---@class gopher.RunnerOpts
---@field cwd? string
---@field timeout? number
---@field stdin? boolean|string|string[]
---@field text? boolean
---@param cmd (string|number)[]
---@param on_exit fun(out:vim.SystemCompleted)
---@param opts? gopher.RunnerOpts
---@return vim.SystemObj
function runner.async(cmd, on_exit, opts)
opts = opts or {}
return vim.system(cmd, {
cwd = opts.cwd or nil,
timeout = opts.timeout or c.timeout,
stdin = opts.stdin or nil,
text = opts.text or true,
}, on_exit)
end
---@param cmd (string|number)[]
---@param opts? gopher.RunnerOpts
---@return vim.SystemCompleted
function runner.sync(cmd, opts)
opts = opts or {}
return vim
.system(cmd, {
cwd = opts.cwd or nil,
timeout = opts.timeout or c.timeout,
stdin = opts.stdin or nil,
text = opts.text or true,
})
:wait()
end
return runner

View file

@ -1,33 +0,0 @@
local Job = require "plenary.job"
local runner = {}
---@class gopher.RunnerOpts
---@field args? string[]
---@field cwd? string?
---@field on_exit? fun(data:string, status:number)
---@param cmd string
---@param opts gopher.RunnerOpts
---@return string[]|nil
function runner.sync(cmd, opts)
local output
Job:new({
command = cmd,
args = opts.args,
cwd = opts.cwd,
on_stderr = function(_, data)
vim.print(data)
end,
on_exit = function(data, status)
output = data:result()
vim.schedule(function()
if opts.on_exit then
opts.on_exit(output, status)
end
end)
end,
}):sync(60000 --[[1 min]])
return output
end
return runner

136
lua/gopher/_utils/ts.lua Normal file
View file

@ -0,0 +1,136 @@
local ts = {}
local queries = {
struct = [[
[(type_spec name: (type_identifier) @_name
type: (struct_type))
(var_declaration (var_spec
name: (identifier) @_name @_var
type: (struct_type)))
(short_var_declaration
left: (expression_list (identifier) @_name @_var)
right: (expression_list (composite_literal
type: (struct_type))))]
]],
func = [[
[(function_declaration name: (identifier) @_name)
(method_declaration name: (field_identifier) @_name)]
]],
package = [[
(package_identifier) @_name
]],
interface = [[
(type_spec
name: (type_identifier) @_name
type: (interface_type))
]],
}
---@param parent_type string[]
---@param node TSNode
---@return TSNode?
local function get_parrent_node(parent_type, node)
---@type TSNode?
local current = node
while current do
if vim.tbl_contains(parent_type, current:type()) then
break
end
current = current:parent()
if current == nil then
return nil
end
end
return current
end
---@param query vim.treesitter.Query
---@param node TSNode
---@param bufnr integer
---@return {name:string, is_varstruct:boolean}
local function get_captures(query, node, bufnr)
local res = {}
for id, _node in query:iter_captures(node, bufnr) do
if query.captures[id] == "_name" then
res["name"] = vim.treesitter.get_node_text(_node, bufnr)
end
if query.captures[id] == "_var" then
res["is_varstruct"] = true
end
end
return res
end
---@class gopher.TsResult
---@field name string
---@field start_line integer
---@field end_line integer
---@field is_varstruct boolean
---@param bufnr integer
---@param parent_type string[]
---@param query string
---@return gopher.TsResult
local function do_stuff(bufnr, parent_type, query)
if not vim.treesitter.get_parser(bufnr, "go") then
error "No treesitter parser found for go"
end
local node = vim.treesitter.get_node {
bufnr = bufnr,
}
if not node then
error "No nodes found under cursor"
end
local parent_node = get_parrent_node(parent_type, node)
if not parent_node then
error "No parent node found under cursor"
end
local q = vim.treesitter.query.parse("go", query)
local res = get_captures(q, parent_node, bufnr)
assert(res.name ~= nil, "No capture name found")
local start_row, _, end_row, _ = parent_node:range()
res["start_line"] = start_row + 1
res["end_line"] = end_row + 1
return res
end
---@param bufnr integer
function ts.get_struct_under_cursor(bufnr)
--- should be both type_spec and type_declaration
--- because in cases like `type ( T struct{}, U strict{} )`
--- i will be choosing always last struct in the list
---
--- var_declaration is for cases like `var x struct{}`
--- short_var_declaration is for cases like `x := struct{}{}`
return do_stuff(bufnr, {
"type_spec",
"type_declaration",
"var_declaration",
"short_var_declaration",
}, queries.struct)
end
---@param bufnr integer
function ts.get_func_under_cursor(bufnr)
--- since this handles both and funcs and methods we should check for both parent nodes
return do_stuff(bufnr, { "function_declaration", "method_declaration" }, queries.func)
end
---@param bufnr integer
function ts.get_package_under_cursor(bufnr)
return do_stuff(bufnr, { "package_clause" }, queries.package)
end
---@param bufnr integer
function ts.get_interface_under_cursor(bufnr)
return do_stuff(bufnr, { "type_declaration" }, queries.interface)
end
return ts

View file

@ -1,104 +0,0 @@
---@diagnostic disable: param-type-mismatch
local nodes = require "gopher._utils.ts.nodes"
local u = require "gopher._utils"
local ts = {
querys = {
struct_block = [[((type_declaration (type_spec name:(type_identifier) @struct.name type: (struct_type)))@struct.declaration)]],
em_struct_block = [[(field_declaration name:(field_identifier)@struct.name type: (struct_type)) @struct.declaration]],
package = [[(package_clause (package_identifier)@package.name)@package.clause]],
interface = [[((type_declaration (type_spec name:(type_identifier) @interface.name type:(interface_type)))@interface.declaration)]],
method_name = [[((method_declaration receiver: (parameter_list)@method.receiver name: (field_identifier)@method.name body:(block))@method.declaration)]],
func = [[((function_declaration name: (identifier)@function.name) @function.declaration)]],
},
}
---@return table
local function get_name_defaults()
return {
["func"] = "function",
["if"] = "if",
["else"] = "else",
["for"] = "for",
}
end
---@param row string
---@param col string
---@param bufnr string|nil
---@param do_notify boolean|nil
---@return table|nil
function ts.get_struct_node_at_pos(row, col, bufnr, do_notify)
local notify = do_notify or true
local query = ts.querys.struct_block .. " " .. ts.querys.em_struct_block
local bufn = bufnr or vim.api.nvim_get_current_buf()
local ns = nodes.nodes_at_cursor(query, get_name_defaults(), bufn, row, col)
if ns == nil then
if notify then
u.deferred_notify("struct not found", vim.log.levels.WARN)
end
else
return ns[#ns]
end
end
---@param row string
---@param col string
---@param bufnr string|nil
---@param do_notify boolean|nil
---@return table|nil
function ts.get_func_method_node_at_pos(row, col, bufnr, do_notify)
local notify = do_notify or true
local query = ts.querys.func .. " " .. ts.querys.method_name
local bufn = bufnr or vim.api.nvim_get_current_buf()
local ns = nodes.nodes_at_cursor(query, get_name_defaults(), bufn, row, col)
if ns == nil then
if notify then
u.deferred_notify("function not found", vim.log.levels.WARN)
end
else
return ns[#ns]
end
end
---@param row string
---@param col string
---@param bufnr string|nil
---@param do_notify boolean|nil
---@return table|nil
function ts.get_package_node_at_pos(row, col, bufnr, do_notify)
local notify = do_notify or true
-- stylua: ignore
if row > 10 then return end
local query = ts.querys.package
local bufn = bufnr or vim.api.nvim_get_current_buf()
local ns = nodes.nodes_at_cursor(query, get_name_defaults(), bufn, row, col)
if ns == nil then
if notify then
u.deferred_notify("package not found", vim.log.levels.WARN)
return nil
end
else
return ns[#ns]
end
end
---@param row string
---@param col string
---@param bufnr string|nil
---@param do_notify boolean|nil
---@return table|nil
function ts.get_interface_node_at_pos(row, col, bufnr, do_notify)
local notify = do_notify or true
local query = ts.querys.interface
local bufn = bufnr or vim.api.nvim_get_current_buf()
local ns = nodes.nodes_at_cursor(query, get_name_defaults(), bufn, row, col)
if ns == nil then
if notify then
u.deferred_notify("interface not found", vim.log.levels.WARN)
end
else
return ns[#ns]
end
end
return ts

View file

@ -1,143 +0,0 @@
local ts_query = require "nvim-treesitter.query"
local parsers = require "nvim-treesitter.parsers"
local locals = require "nvim-treesitter.locals"
local u = require "gopher._utils"
local M = {}
local function intersects(row, col, sRow, sCol, eRow, eCol)
if sRow > row or eRow < row then
return false
end
if sRow == row and sCol > col then
return false
end
if eRow == row and eCol < col then
return false
end
return true
end
---@param nodes table
---@param row string
---@param col string
---@return table
function M.intersect_nodes(nodes, row, col)
local found = {}
for idx = 1, #nodes do
local node = nodes[idx]
local sRow = node.dim.s.r
local sCol = node.dim.s.c
local eRow = node.dim.e.r
local eCol = node.dim.e.c
if intersects(row, col, sRow, sCol, eRow, eCol) then
table.insert(found, node)
end
end
return found
end
---@param nodes table
---@return table
function M.sort_nodes(nodes)
table.sort(nodes, function(a, b)
return M.count_parents(a) < M.count_parents(b)
end)
return nodes
end
---@param query string
---@param lang string
---@param bufnr integer
---@param pos_row string
---@return string
function M.get_all_nodes(query, lang, _, bufnr, pos_row, _)
bufnr = bufnr or 0
pos_row = pos_row or 30000
local ok, parsed_query = pcall(function()
return vim.treesitter.query.parse(lang, query)
end)
if not ok then
return nil
end
local parser = parsers.get_parser(bufnr, lang)
local root = parser:parse()[1]:root()
local start_row, _, end_row, _ = root:range()
local results = {}
for match in ts_query.iter_prepared_matches(parsed_query, root, bufnr, start_row, end_row) do
local sRow, sCol, eRow, eCol, declaration_node
local type, name, op = "", "", ""
locals.recurse_local_nodes(match, function(_, node, path)
local idx = string.find(path, ".[^.]*$")
op = string.sub(path, idx + 1, #path)
type = string.sub(path, 1, idx - 1)
if op == "name" then
name = vim.treesitter.get_node_text(node, bufnr)
elseif op == "declaration" or op == "clause" then
declaration_node = node
sRow, sCol, eRow, eCol = node:range()
sRow = sRow + 1
eRow = eRow + 1
sCol = sCol + 1
eCol = eCol + 1
end
end)
if declaration_node ~= nil then
table.insert(results, {
declaring_node = declaration_node,
dim = { s = { r = sRow, c = sCol }, e = { r = eRow, c = eCol } },
name = name,
operator = op,
type = type,
})
end
end
return results
end
---@param query string
---@param default string
---@param bufnr string
---@param row string
---@param col string
---@return table
function M.nodes_at_cursor(query, default, bufnr, row, col)
bufnr = bufnr or vim.api.nvim_get_current_buf()
local ft = vim.api.nvim_buf_get_option(bufnr, "ft")
if row == nil or col == nil then
row, col = unpack(vim.api.nvim_win_get_cursor(0))
end
local nodes = M.get_all_nodes(query, ft, default, bufnr, row, col)
if nodes == nil then
u.deferred_notify(
"Unable to find any nodes. Place your cursor on a go symbol and try again",
vim.log.levels.DEBUG
)
return nil
end
nodes = M.sort_nodes(M.intersect_nodes(nodes, row, col))
if nodes == nil or #nodes == 0 then
u.deferred_notify(
"Unable to find any nodes at pos. " .. tostring(row) .. ":" .. tostring(col),
vim.log.levels.DEBUG
)
return nil
end
return nodes
end
return M

View file

@ -1,59 +1,57 @@
---@toc_entry Generate comments
---@tag gopher.nvim-comments
---@usage Execute `:GoCmt` to generate a comment for the current function/method/struct/etc on this line.
---@text This module provides a way to generate comments for Go code.
---@text
--- This module provides a way to generate comments for Go code.
---
---@usage Set cursor on line with function/method/struct/etc and run `:GoCmt` to generate a comment.
local ts = require "gopher._utils.ts"
local log = require "gopher._utils.log"
local comment = {}
local function generate(row, col)
local ts_utils = require "gopher._utils.ts"
local comment, ns = nil, nil
ns = ts_utils.get_package_node_at_pos(row, col, nil, false)
if ns ~= nil then
comment = "// Package " .. ns.name .. " provides " .. ns.name
return comment, ns
end
ns = ts_utils.get_struct_node_at_pos(row, col, nil, false)
if ns ~= nil then
comment = "// " .. ns.name .. " " .. ns.type .. " "
return comment, ns
end
ns = ts_utils.get_func_method_node_at_pos(row, col, nil, false)
if ns ~= nil then
comment = "// " .. ns.name .. " " .. ns.type .. " "
return comment, ns
end
ns = ts_utils.get_interface_node_at_pos(row, col, nil, false)
if ns ~= nil then
comment = "// " .. ns.name .. " " .. ns.type .. " "
return comment, ns
end
return "// ", {}
---@param name string
---@return string
---@dochide
local function template(name)
return "// " .. name .. " "
end
return function()
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
local comment, ns = generate(row + 1, col + 1)
---@param bufnr integer
---@return string
---@dochide
local function generate(bufnr)
local s_ok, s_res = pcall(ts.get_struct_under_cursor, bufnr)
if s_ok then
return template(s_res.name)
end
log.debug("generated comment: " .. comment)
local f_ok, f_res = pcall(ts.get_func_under_cursor, bufnr)
if f_ok then
return template(f_res.name)
end
vim.api.nvim_win_set_cursor(0, {
ns.dim.s.r,
ns.dim.s.c,
})
local i_ok, i_res = pcall(ts.get_interface_under_cursor, bufnr)
if i_ok then
return template(i_res.name)
end
---@diagnostic disable-next-line: param-type-mismatch
vim.fn.append(row - 1, comment)
local p_ok, p_res = pcall(ts.get_package_under_cursor, bufnr)
if p_ok then
return "// Package " .. p_res.name .. " provides "
end
vim.api.nvim_win_set_cursor(0, {
ns.dim.s.r,
#comment + 1,
})
vim.cmd [[startinsert!]]
return "// "
end
function comment.comment()
local bufnr = vim.api.nvim_get_current_buf()
local cmt = generate(bufnr)
log.debug("generated comment: " .. cmt)
local pos = vim.fn.getcurpos()[2]
vim.fn.append(pos - 1, cmt)
vim.fn.setpos(".", { 0, pos, #cmt })
vim.cmd "startinsert!"
end
return comment

View file

@ -1,17 +1,9 @@
---@toc_entry Configuration
---@tag gopher.nvim-config
---@text config it is the place where you can configure the plugin.
--- also this is optional is you're ok with default settings.
--- You can look at default options |gopher.nvim-config-defaults|
---@type gopher.Config
---@private
local config = {}
---@tag gopher.nvim-config.ConfigGoTagTransform
---@text Possible values for |gopher.Config|.gotag.transform:
---
---@private
---@dochide
---@alias gopher.ConfigGoTagTransform
---| "snakecase" "GopherUser" -> "gopher_user"
---| "camelcase" "GopherUser" -> "gopherUser"
@ -20,19 +12,19 @@ local config = {}
---| "titlecase" "GopherUser" -> "Gopher User"
---| "keep" keeps the original field name
--minidoc_replace_start {
---@tag gopher.nvim-config-defaults
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section):gsub(">", ">lua")
---
---@toc_entry Config
---@tag gopher.nvim-config
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
---@class gopher.Config
local default_config = {
--minidoc_replace_end
-- log level, you might consider using DEBUG or TRACE for degugging the plugin
-- log level, you might consider using DEBUG or TRACE for debugging the plugin
---@type number
log_level = vim.log.levels.INFO,
-- timeout for running internal commands
---@type number
timeout = 2000,
-- user specified paths to binaries
---@class gopher.ConfigCommand
commands = {
@ -41,7 +33,6 @@ local default_config = {
gotests = "gotests",
impl = "impl",
iferr = "iferr",
dlv = "dlv",
},
---@class gopher.ConfigGotests
gotests = {
@ -51,33 +42,61 @@ local default_config = {
---@type string|nil
template_dir = nil,
-- switch table tests from using slice to map (with test name for the key)
-- works only with gotests installed from develop branch
named = false,
},
---@class gopher.ConfigGoTag
gotag = {
---@type gopher.ConfigGoTagTransform
transform = "snakecase",
-- default tags to add to struct fields
default_tag = "json",
},
iferr = {
-- choose a custom error message
---@type string|nil
message = nil,
},
}
--minidoc_afterlines_end
---@type gopher.Config
---@private
---@dochide
local _config = default_config
-- I am kinda secret so don't tell anyone about me
-- even dont use me
-- I am kinda secret so don't tell anyone about me even dont use me
--
-- if you don't belive me that i am secret see
-- if you don't believe me that i am secret see
-- the line below it says @private
---@private
_config.___plugin_name = "gopher.nvim" ---@diagnostic disable-line: inject-field
---@param user_config? gopher.Config
---@private
---@dochide
function config.setup(user_config)
_config = vim.tbl_deep_extend("force", default_config, user_config or {})
vim.validate { user_config = { user_config, "table", true } }
_config = vim.tbl_deep_extend("force", vim.deepcopy(default_config), user_config or {})
vim.validate {
log_level = { _config.log_level, "number" },
timeout = { _config.timeout, "number" },
["commands"] = { _config.commands, "table" },
["commands.go"] = { _config.commands.go, "string" },
["commands.gomodifytags"] = { _config.commands.gomodifytags, "string" },
["commands.gotests"] = { _config.commands.gotests, "string" },
["commands.impl"] = { _config.commands.impl, "string" },
["commands.iferr"] = { _config.commands.iferr, "string" },
["gotests"] = { _config.gotests, "table" },
["gotests.template"] = { _config.gotests.template, "string" },
["gotests.template_dir"] = { _config.gotests.template, "string", true },
["gotests.named"] = { _config.gotests.named, "boolean" },
["gotag"] = { _config.gotag, "table" },
["gotag.transform"] = { _config.gotag.transform, "string" },
["gotag.default_tag"] = { _config.gotag.default_tag, "string" },
["iferr"] = { _config.iferr, "table" },
["iferr.message"] = { _config.iferr.message, "string", true },
}
end
setmetatable(config, {
@ -86,4 +105,6 @@ setmetatable(config, {
end,
})
---@dochide
---@return gopher.Config
return config

View file

@ -1,129 +0,0 @@
---@toc_entry Setup `nvim-dap` for Go
---@tag gopher.nvim-dap
---@text This module sets up `nvim-dap` for Go.
---@usage just call `require("gopher.dap").setup()`, and you're good to go.
local c = require "gopher.config"
local dap = {}
dap.adapter = function(callback, config)
local host = config.host or "127.0.0.1"
local port = config.port or "38697"
local addr = string.format("%s:%s", host, port)
local handle, pid_or_err
local stdout = assert(vim.loop.new_pipe(false))
local opts = {
stdio = { nil, stdout },
args = { "dap", "-l", addr },
detached = true,
}
handle, pid_or_err = vim.loop.spawn(c.commands.dlv, opts, function(status)
if not stdout or not handle then
return
end
stdout:close()
handle:close()
if status ~= 0 then
print("dlv exited with code", status)
end
end)
assert(handle, "Error running dlv: " .. tostring(pid_or_err))
if stdout then
stdout:read_start(function(err, chunk)
assert(not err, err)
if chunk then
vim.schedule(function()
require("dap.repl").append(chunk)
end)
end
end)
end
-- wait for delve to start
vim.defer_fn(function()
callback { type = "server", host = "127.0.0.1", port = port }
end, 100)
end
local function args_input()
vim.ui.input({ prompt = "Args: " }, function(input)
return vim.split(input or "", " ")
end)
end
local function get_arguments()
local co = coroutine.running()
if co then
return coroutine.create(function()
local args = args_input()
coroutine.resume(co, args)
end)
else
return args_input()
end
end
dap.configuration = {
{
type = "go",
name = "Debug",
request = "launch",
program = "${file}",
},
{
type = "go",
name = "Debug (Arguments)",
request = "launch",
program = "${file}",
args = get_arguments,
},
{
type = "go",
name = "Debug Package",
request = "launch",
program = "${fileDirname}",
},
{
type = "go",
name = "Attach",
mode = "local",
request = "attach",
processId = require("dap.utils").pick_process,
},
{
type = "go",
name = "Debug test",
request = "launch",
mode = "test",
program = "${file}",
},
{
type = "go",
name = "Debug test (go.mod)",
request = "launch",
mode = "test",
program = "./${relativeFileDirname}",
},
}
-- sets ups nvim-dap for Go in one function call.
function dap.setup()
vim.deprecate(
"gopher.dap",
"you might consider setting up `nvim-dap` manually, or using another plugin(https://github.com/leoluz/nvim-dap-go)",
"v0.1.6",
"gopher"
)
local ok, d = pcall(require, "dap")
assert(ok, "gopher.nvim dependency error: dap not installed")
d.adapters.go = dap.adapter
d.configurations.go = dap.configuration
end
return dap

View file

@ -2,42 +2,20 @@
---@tag gopher.nvim-gotests
---@text gotests is utilizing the `gotests` tool to generate unit tests boilerplate.
---@usage
--- - generate unit test for spesisfic function/method
--- - to specift the function/method put your cursor on it
--- - run `:GoTestAdd`
--- - Generate unit test for specific function/method:
--- 1. Place your cursor on the desired function/method.
--- 2. Run `:GoTestAdd`
---
--- - generate unit tests for all functions/methods in current file
--- - Generate unit tests for *all* functions/methods in current file:
--- - run `:GoTestsAll`
---
--- - generate unit tests only for exported(public) functions/methods
--- - Generate unit tests *only* for *exported(public)* functions/methods:
--- - run `:GoTestsExp`
---
--- you can also specify the template to use for generating the tests. see |gopher.nvim-config|
--- more details about templates can be found at: https://github.com/cweill/gotests
--- You can also specify the template to use for generating the tests. See |gopher.nvim-config|
--- More details about templates can be found at: https://github.com/cweill/gotests
---
---@tag gopher.nvim-gotests-named
---@text
--- if you prefare using named tests, you can enable it in the config.
--- but you would need to install `gotests@develop` because stable version doesn't support this feature.
--- you can do it with:
--- >lua
--- -- simply run go get in your shell:
--- go install github.com/cweill/gotests/...@develop
---
--- -- if you want to install it within neovim, you can use one of this:
---
--- vim.fn.jobstart("go install github.com/cweill/gotests/...@develop")
---
--- -- or if you want to use mason:
--- require("mason-tool-installer").setup {
--- ensure_installed = {
--- { "gotests", version = "develop" },
--- }
--- }
--- <
---
--- if you choose to install `gotests` within neovim, i recommend adding it to your `build` section in your |lazy.nvim|
--- If you prefer named tests, you can enable them in |gopher.nvim-config|.
local c = require "gopher.config"
local ts_utils = require "gopher._utils.ts"
@ -47,7 +25,7 @@ local log = require "gopher._utils.log"
local gotests = {}
---@param args table
---@private
---@dochide
local function add_test(args)
if c.gotests.named then
table.insert(args, "-named")
@ -68,27 +46,20 @@ local function add_test(args)
log.debug("generating tests with args: ", args)
return r.sync(c.commands.gotests, {
args = args,
on_exit = function(data, status)
if not status == 0 then
error("gotests failed: " .. data)
local rs = r.sync { c.commands.gotests, unpack(args) }
if rs.code ~= 0 then
error("gotests failed: " .. rs.stderr)
end
u.notify "unit test(s) generated"
end,
})
end
-- generate unit test for one function
function gotests.func_test()
local ns = ts_utils.get_func_method_node_at_pos(unpack(vim.api.nvim_win_get_cursor(0)))
if ns == nil or ns.name == nil then
u.notify("cursor on func/method and execute the command again", vim.log.levels.WARN)
return
end
local bufnr = vim.api.nvim_get_current_buf()
local func = ts_utils.get_func_under_cursor(bufnr)
add_test { "-only", ns.name }
add_test { "-only", func.name }
end
-- generate unit tests for all functions in current file

View file

@ -1,12 +1,9 @@
local health = {}
local cmd = require("gopher.config").commands
local u = require "gopher._utils.health_util"
local deps = {
plugin = {
{ lib = "dap", msg = "required for `gopher.dap`", optional = true },
{ lib = "plenary", msg = "required for everyting in gopher.nvim", optional = false },
{ lib = "nvim-treesitter", msg = "required for everyting in gopher.nvim", optional = false },
{ lib = "nvim-treesitter", msg = "required for everything in gopher.nvim" },
},
bin = {
{
@ -14,55 +11,70 @@ local deps = {
msg = "required for `:GoGet`, `:GoMod`, `:GoGenerate`, `:GoWork`, `:GoInstallDeps`",
optional = false,
},
{ bin = cmd.gomodifytags, msg = "required for `:GoTagAdd`, `:GoTagRm`", optional = false },
{ bin = cmd.impl, msg = "required for `:GoImpl`", optional = false },
{ bin = cmd.iferr, msg = "required for `:GoIfErr`", optional = false },
{ bin = cmd.gomodifytags, msg = "required for `:GoTagAdd`, `:GoTagRm`", optional = true },
{ bin = cmd.impl, msg = "required for `:GoImpl`", optional = true },
{ bin = cmd.iferr, msg = "required for `:GoIfErr`", optional = true },
{
bin = cmd.gotests,
msg = "required for `:GoTestAdd`, `:GoTestsAll`, `:GoTestsExp`",
optional = false,
optional = true,
},
{ bin = cmd.dlv, msg = "required for debugging, (`nvim-dap`, `gopher.dap`)", optional = true },
},
treesitter = {
{ parser = "go", msg = "required for `gopher.nvim`", optional = false },
{ parser = "go", msg = "required for `gopher.nvim`" },
},
}
---@param module string
---@return boolean
local function is_lualib_found(module)
local is_found, _ = pcall(require, module)
return is_found
end
---@param bin string
---@return boolean
local function is_binary_found(bin)
return vim.fn.executable(bin) == 1
end
---@param ft string
---@return boolean
local function is_treesitter_parser_available(ft)
local ok, parser = pcall(vim.treesitter.get_parser, 0, ft)
return ok and parser ~= nil
end
function health.check()
u.start "required plugins"
vim.health.start "required plugins"
for _, plugin in ipairs(deps.plugin) do
if u.is_lualib_found(plugin.lib) then
u.ok(plugin.lib .. " installed")
if is_lualib_found(plugin.lib) then
vim.health.ok(plugin.lib .. " installed")
else
if plugin.optional then
u.warn(plugin.lib .. " not found, " .. plugin.msg)
else
u.error(plugin.lib .. " not found, " .. plugin.msg)
end
vim.health.error(plugin.lib .. " not found, " .. plugin.msg)
end
end
u.start "required binaries"
u.info "all those binaries can be installed by `:GoInstallDeps`"
vim.health.start "required binaries"
vim.health.info "all those binaries can be installed by `:GoInstallDeps`"
for _, bin in ipairs(deps.bin) do
if u.is_binary_found(bin.bin) then
u.ok(bin.bin .. " installed")
if is_binary_found(bin.bin) then
vim.health.ok(bin.bin .. " installed")
else
if bin.optional then
u.warn(bin.bin .. " not found, " .. bin.msg)
vim.health.warn(bin.bin .. " not found, " .. bin.msg)
else
u.error(bin.bin .. " not found, " .. bin.msg)
vim.health.error(bin.bin .. " not found, " .. bin.msg)
end
end
end
u.start "required treesitter parsers"
vim.health.start "required treesitter parsers"
for _, parser in ipairs(deps.treesitter) do
if u.is_treesitter_parser_available(parser.parser) then
u.ok(parser.parser .. " parser installed")
if is_treesitter_parser_available(parser.parser) then
vim.health.ok(parser.parser .. " parser installed")
else
u.error(parser.parser .. " parser not found, " .. parser.msg)
vim.health.error(parser.parser .. " parser not found, " .. parser.msg)
end
end
end

View file

@ -1,30 +1,46 @@
-- Thanks https://github.com/koron/iferr for vim implementation
---@toc_entry Iferr
---@tag gopher.nvim-iferr
---@text if you're using `iferr` tool, this module provides a way to automatically insert `if err != nil` check.
---@usage execute `:GoIfErr` near any err variable to insert the check
---@text
--- `iferr` provides a way to way to automatically insert `if err != nil` check.
--- If you want to change `-message` option of `iferr` tool, see |gopher.nvim-config|
---
---@usage Execute `:GoIfErr` near any `err` variable to insert the check
local c = require "gopher.config"
local u = require "gopher._utils"
local r = require "gopher._utils.runner"
local log = require "gopher._utils.log"
local iferr = {}
-- That's Lua implementation: github.com/koron/iferr
function iferr.iferr()
local boff = vim.fn.wordcount().cursor_bytes
local curb = vim.fn.wordcount().cursor_bytes
local pos = vim.fn.getcurpos()[2]
local fpath = vim.fn.expand "%"
local data = vim.fn.systemlist((c.commands.iferr .. " -pos " .. boff), vim.fn.bufnr "%")
if vim.v.shell_error ~= 0 then
if string.find(data[1], "no functions at") then
vim.print "no function found"
log.warn("iferr: no function at " .. boff)
local cmd = { c.commands.iferr, "-pos", curb }
if c.iferr.message ~= nil and type(c.iferr.message) == "string" then
table.insert(cmd, "-message")
table.insert(cmd, c.iferr.message)
end
local rs = r.sync(cmd, {
stdin = u.readfile_joined(fpath),
})
if rs.code ~= 0 then
if string.find(rs.stderr, "no functions at") then
u.notify("iferr: no function at " .. curb, vim.log.levels.ERROR)
log.warn("iferr: no function at " .. curb)
return
end
log.error("failed. output: " .. vim.inspect(data))
error("iferr failed: " .. vim.inspect(data))
log.error("ferr: failed. output: " .. rs.stderr)
error("iferr failed: " .. rs.stderr)
end
vim.fn.append(pos, data)
vim.fn.append(pos, u.remove_empty_lines(vim.split(rs.stdout, "\n")))
vim.cmd [[silent normal! j=2j]]
vim.fn.setpos(".", pos)
end

View file

@ -1,20 +1,27 @@
---@toc_entry Auto implementation of interface methods
---@tag gopher.nvim-impl
---@text impl is utilizing the `impl` tool to generate method stubs for interfaces.
---@usage
--- 1. put your coursor on the struct on which you want implement the interface
--- and run `:GoImpl io.Reader`
--- which will automatically choose the reciver for the methods and
--- implement the `io.Reader` interface
--- 2. same as previous but with custom receiver, so put your coursor on the struct
--- run `:GoImpl w io.Writer`
--- where `w` is the receiver and `io.Writer` is the interface
--- 3. specift receiver, struct, and interface
--- there's no need to put your coursor on the struct if you specify all arguments
--- `:GoImpl r RequestReader io.Reader`
--- where `r` is the receiver, `RequestReader` is the struct and `io.Reader` is the interface
---@text
--- Integration of `impl` tool to generate method stubs for interfaces.
---
--- simple example:
---@usage 1. Automatically implement an interface for a struct:
--- - Place your cursor on the struct where you want to implement the interface.
--- - Run `:GoImpl io.Reader`
--- - This will automatically determine the receiver and implement the `io.Reader` interface.
---
--- 2. Specify a custom receiver:
--- - Place your cursor on the struct
--- - Run `:GoImpl w io.Writer`, where:
--- - `w` is the receiver.
--- - `io.Writer` is the interface to implement.
---
--- 3. Explicitly specify the receiver, struct, and interface:
--- - No need to place the cursor on the struct if all arguments are provided.
--- - Run `:GoImpl r RequestReader io.Reader`, where:
--- - `r` is the receiver.
--- - `RequestReader` is the struct.
--- - `io.Reader` is the interface to implement.
---
--- Example:
--- >go
--- type BytesReader struct{}
--- // ^ put your cursor here
@ -32,65 +39,32 @@ local ts_utils = require "gopher._utils.ts"
local u = require "gopher._utils"
local impl = {}
---@return string
---@private
local function get_struct()
local ns = ts_utils.get_struct_node_at_pos(unpack(vim.api.nvim_win_get_cursor(0)))
if ns == nil then
u.deferred_notify("put cursor on a struct or specify a receiver", vim.log.levels.INFO)
return ""
end
vim.api.nvim_win_set_cursor(0, {
ns.dim.e.r,
ns.dim.e.c,
})
return ns.name
end
function impl.impl(...)
local args = { ... }
local iface, recv_name = "", ""
local recv = get_struct()
local iface, recv = "", ""
local bufnr = vim.api.nvim_get_current_buf()
if #args == 0 then
iface = vim.fn.input "impl: generating method stubs for interface: "
vim.cmd "redraw!"
if iface == "" then
u.deferred_notify("usage: GoImpl f *File io.Reader", vim.log.levels.INFO)
return
end
elseif #args == 1 then -- :GoImpl io.Reader
recv = string.lower(recv) .. " *" .. recv
vim.cmd "redraw!"
iface = select(1, ...)
if #args == 1 then -- :GoImpl io.Reader
local st = ts_utils.get_struct_under_cursor(bufnr)
iface = args[1]
recv = string.lower(st.name) .. " *" .. st.name
elseif #args == 2 then -- :GoImpl w io.Writer
recv_name = select(1, ...)
recv = string.format("%s *%s", recv_name, recv)
iface = select(#args, ...)
elseif #args > 2 then
iface = select(#args, ...)
recv = select(#args - 1, ...)
recv_name = select(#args - 2, ...)
recv = string.format("%s %s", recv_name, recv)
local st = ts_utils.get_struct_under_cursor(bufnr)
iface = args[2]
recv = args[1] .. " *" .. st.name
elseif #args == 3 then -- :GoImpl r Struct io.Reader
recv = args[1] .. " *" .. args[2]
iface = args[3]
end
local output = r.sync(c.impl, {
args = {
"-dir",
vim.fn.fnameescape(vim.fn.expand "%:p:h" --[[@as string]]),
recv,
iface,
},
on_exit = function(data, status)
if not status == 0 then
error("impl failed: " .. data)
local rs = r.sync { c.impl, "-dir", vim.fn.fnameescape(vim.fn.expand "%:p:h"), recv, iface }
if rs.code ~= 0 then
error("failed to implement interface: " .. rs.stderr)
end
end,
})
local pos = vim.fn.getcurpos()[2]
local output = u.remove_empty_lines(vim.split(rs.stdout, "\n"))
table.insert(output, 1, "")
vim.fn.append(pos, output)
end

View file

@ -1,28 +1,32 @@
--- *gopher.nvim*
--- *gopher.nvim* Enhance your golang experience
---
--- MIT License Copyright (c) 2025 Oleksandr Smirnov
---
--- ==============================================================================
---
--- gopher.nvim is a minimalistic plugin for Go development in Neovim written in Lua.
--- It's not an LSP tool, the main goal of this plugin is add go tooling support in Neovim.
---
--- Table of Contents
---@tag gopher.nvim-table-of-contents
---@toc
local log = require "gopher._utils.log"
local tags = require "gopher.struct_tags"
local tests = require "gopher.gotests"
local gocmd = require("gopher._utils.runner.gocmd").run
local gocmd = require("gopher._utils.gocmd").run
local gopher = {}
---@toc_entry Setup
---@tag gopher.nvim-setup
---@text Setup function. This method simply merges default configs with opts table.
---@tag gopher.nvim-setup()
---@text Setup function. This method simply merges default config with opts table.
--- You can read more about configuration at |gopher.nvim-config|
--- Calling this function is optional, if you ok with default settings. Look |gopher.nvim.config-defaults|
--- Calling this function is optional, if you ok with default settings.
--- See |gopher.nvim.config|
---
---@usage `require("gopher").setup {}` (replace `{}` with your `config` table)
---@param user_config gopher.Config
---@usage >lua
--- require("gopher").setup {} -- use default config or replace {} with your own
--- <
---@param user_config gopher.Config See |gopher.nvim-config|
gopher.setup = function(user_config)
log.debug "setting up config"
require("gopher.config").setup(user_config)
@ -30,19 +34,22 @@ gopher.setup = function(user_config)
end
---@toc_entry Install dependencies
---@tag gopher.nvim-install-deps
---@tag gopher.nvim-dependencies
---@text Gopher.nvim implements most of its features using third-party tools.
--- To install these tools, you can run `:GoInstallDeps` command
--- or call `require("gopher").install_deps()` if you want ues lua api.
--- or call `require("gopher").install_deps()` if you want to use lua api.
--- By default dependencies will be installed asynchronously,
--- to install them synchronously pass `{sync = true}` as an argument.
gopher.install_deps = require("gopher.installer").install_deps
gopher.impl = require("gopher.impl").impl
gopher.iferr = require("gopher.iferr").iferr
gopher.comment = require "gopher.comment"
gopher.comment = require("gopher.comment").comment
gopher.tags = {
add = tags.add,
rm = tags.remove,
clear = tags.clear,
}
gopher.test = {
@ -52,19 +59,19 @@ gopher.test = {
}
gopher.get = function(...)
gocmd("get", { ... })
gocmd("get", ...)
end
gopher.mod = function(...)
gocmd("mod", { ... })
gocmd("mod", ...)
end
gopher.generate = function(...)
gocmd("generate", { ... })
gocmd("generate", ...)
end
gopher.work = function(...)
gocmd("work", { ... })
gocmd("work", ...)
end
return gopher

View file

@ -1,35 +1,56 @@
local c = require("gopher.config").commands
local r = require "gopher._utils.runner"
local u = require "gopher._utils"
local log = require "gopher._utils.log"
local installer = {}
local urls = {
gomodifytags = "github.com/fatih/gomodifytags",
impl = "github.com/josharian/impl",
gotests = "github.com/cweill/gotests/...",
iferr = "github.com/koron/iferr",
dlv = "github.com/go-delve/delve/cmd/dlv",
gomodifytags = "github.com/fatih/gomodifytags@latest",
impl = "github.com/josharian/impl@latest",
gotests = "github.com/cweill/gotests/...@develop",
iferr = "github.com/koron/iferr@latest",
}
---@param pkg string
local function install(pkg)
local url = urls[pkg] .. "@latest"
r.sync(c.go, {
args = { "install", url },
on_exit = function(data, status)
if not status == 0 then
error("go install failed: " .. data)
---@param opt vim.SystemCompleted
---@param url string
local function handle_intall_exit(opt, url)
if opt.code ~= 0 then
vim.schedule(function()
u.notify("go install failed: " .. url)
end)
log.error("go install failed:", "url", url, "opt", vim.inspect(opt))
return
end
u.notify("installed: " .. url)
end,
})
vim.schedule(function()
u.notify("go install-ed: " .. url)
end)
end
---@param url string
local function install(url)
r.async({ c.go, "install", url }, function(opt)
handle_intall_exit(opt, url)
end)
end
---@param url string
local function install_sync(url)
local rs = r.sync { c.go, "install", url }
handle_intall_exit(rs, url)
end
---Install required go deps
function installer.install_deps()
for pkg, _ in pairs(urls) do
install(pkg)
---@param opts? {sync:boolean}
function installer.install_deps(opts)
opts = opts or {}
for _, url in pairs(urls) do
if opts.sync then
install_sync(url)
else
install(url)
end
end
end

View file

@ -1,13 +1,19 @@
---@toc_entry Modifty struct tags
---@toc_entry Modify struct tags
---@tag gopher.nvim-struct-tags
---@text struct-tags is utilizing the `gomodifytags` tool to add or remove tags to struct fields.
---@usage - put your coursor on the struct
--- - run `:GoTagAdd json` to add json tags to struct fields
--- - run `:GoTagRm json` to remove json tags to struct fields
---@text
--- `struct_tags` is utilizing the `gomodifytags` tool to add or remove tags to struct fields.
---
--- note: if you dont spesify the tag it will use `json` as default
---@usage
--- How to add/remove tags to struct fields:
--- 1. Place cursor on the struct
--- 2. Run `:GoTagAdd json` to add json tags to struct fields
--- 3. Run `:GoTagRm json` to remove json tags to struct fields
---
--- simple example:
--- To clear all tags from struct run: `:GoTagClear`
---
--- NOTE: if you dont specify the tag it will use `json` as default
---
--- Example:
--- >go
--- // before
--- type User struct {
@ -24,105 +30,109 @@
--- }
--- <
local ts_utils = require "gopher._utils.ts"
local ts = require "gopher._utils.ts"
local r = require "gopher._utils.runner"
local c = require "gopher.config"
local u = require "gopher._utils"
local log = require "gopher._utils.log"
local struct_tags = {}
local function modify(...)
local fpath = vim.fn.expand "%" ---@diagnostic disable-line: missing-parameter
local ns = ts_utils.get_struct_node_at_pos(unpack(vim.api.nvim_win_get_cursor(0)))
if ns == nil then
return
end
---@param fpath string
---@param bufnr integer
---@param user_args string[]
---@dochide
local function handle_tags(fpath, bufnr, user_args)
local st = ts.get_struct_under_cursor(bufnr)
-- stylua: ignore
local cmd_args = {
local cmd = {
c.commands.gomodifytags,
"-transform", c.gotag.transform,
"-format", "json",
"-file", fpath,
"-w"
"-w",
}
-- by struct name of line pos
if ns.name == nil then
local _, csrow, _, _ = unpack(vim.fn.getpos ".")
table.insert(cmd_args, "-line")
table.insert(cmd_args, csrow)
if st.is_varstruct then
table.insert(cmd, "-line")
table.insert(cmd, string.format("%d,%d", st.start_line, st.end_line))
else
table.insert(cmd_args, "-struct")
table.insert(cmd_args, ns.name)
table.insert(cmd, "-struct")
table.insert(cmd, st.name)
end
-- set user args for cmd
local arg = { ... }
for _, v in ipairs(arg) do
table.insert(cmd_args, v)
for _, v in ipairs(user_args) do
table.insert(cmd, v)
end
-- set default tag for "clear tags"
if #arg == 1 and arg[1] ~= "-clear-tags" then
table.insert(cmd_args, "json")
local rs = r.sync(cmd)
if rs.code ~= 0 then
log.error("tags: failed to set tags " .. rs.stderr)
error("failed to set tags " .. rs.stderr)
end
local output = r.sync(c.commands.gomodifytags, {
args = cmd_args,
on_exit = function(data, status)
if not status == 0 then
error("gotag failed: " .. data)
local res = vim.json.decode(rs.stdout)
if res["errors"] then
log.error("tags: got an error " .. vim.inspect(res))
error("failed to set tags " .. vim.inspect(res["errors"]))
end
end,
})
-- decode goted value
local tagged = vim.json.decode(table.concat(output))
if
tagged.errors ~= nil
or tagged.lines == nil
or tagged["start"] == nil
or tagged["start"] == 0
then
error("failed to set tags " .. vim.inspect(tagged))
for i, v in ipairs(res["lines"]) do
res["lines"][i] = u.trimend(v)
end
vim.api.nvim_buf_set_lines(
0,
tagged.start - 1,
tagged.start - 1 + #tagged.lines,
false,
tagged.lines
bufnr,
res["start"] - 1,
res["start"] - 1 + #res["lines"],
true,
res["lines"]
)
vim.cmd "write"
end
-- add tags to struct under cursor
---@param args string[]
---@return string
---@dochide
local function handler_user_args(args)
if #args == 0 then
return c.gotag.default_tag
end
return table.concat(args, ",")
end
-- Adds tags to a struct under the cursor
-- See |gopher.nvim-struct-tags|
---@param ... string Tags to add to the struct fields. If not provided, it will use [config.gotag.default_tag]
---@dochide
function struct_tags.add(...)
local arg = { ... }
if #arg == nil or arg == "" then
arg = { "json" }
end
local args = { ... }
local fpath = vim.fn.expand "%"
local bufnr = vim.api.nvim_get_current_buf()
local cmd_args = { "-add-tags" }
for _, v in ipairs(arg) do
table.insert(cmd_args, v)
end
modify(unpack(cmd_args))
local user_tags = handler_user_args(args)
handle_tags(fpath, bufnr, { "-add-tags", user_tags })
end
-- remove tags to struct under cursor
-- Removes tags from a struct under the cursor
-- See `:h gopher.nvim-struct-tags`
---@dochide
---@param ... string Tags to add to the struct fields. If not provided, it will use [config.gotag.default_tag]
function struct_tags.remove(...)
local arg = { ... }
if #arg == nil or arg == "" then
arg = { "json" }
end
local args = { ... }
local fpath = vim.fn.expand "%"
local bufnr = vim.api.nvim_get_current_buf()
local cmd_args = { "-remove-tags" }
for _, v in ipairs(arg) do
table.insert(cmd_args, v)
end
local user_tags = handler_user_args(args)
handle_tags(fpath, bufnr, { "-remove-tags", user_tags })
end
modify(unpack(cmd_args))
-- Removes all tags from a struct under the cursor
-- See `:h gopher.nvim-struct-tags`
---@dochide
function struct_tags.clear()
local fpath = vim.fn.expand "%"
local bufnr = vim.api.nvim_get_current_buf()
handle_tags(fpath, bufnr, { "-clear-tags" })
end
return struct_tags

View file

@ -1,6 +1,9 @@
[vim]
any = true
[MiniTest]
any = true
[describe]
any = true
[[describe.args]]

13
pkg.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "gopher.nvim",
"engines": {
"nvim": "^0.10.0"
},
"repository": {
"type": "git",
"url": "https://github.com/olexsmir/gopher.nvim"
},
"dependencies": {
"https://github.com/nvim-treesitter/nvim-treesitter": "*"
}
}

87
plugin/gopher.lua Normal file
View file

@ -0,0 +1,87 @@
---@toc_entry Commands
---@tag gopher.nvim-commands
---@text
--- If don't want to automatically register plugins' commands,
--- you can set `vim.g.gopher_register_commands` to `false`, before loading the plugin.
if vim.g.gopher_register_commands == false then
return
end
---@param name string
---@param fn fun(args: table)
---@param nargs? number|"*"|"?"
---@private
local function cmd(name, fn, nargs)
nargs = nargs or 0
vim.api.nvim_create_user_command(name, fn, { nargs = nargs })
end
cmd("GopherLog", function()
vim.cmd("tabnew " .. require("gopher._utils.log").get_outfile())
end)
cmd("GoIfErr", function()
require("gopher").iferr()
end)
cmd("GoCmt", function()
require("gopher").comment()
end)
cmd("GoImpl", function(args)
require("gopher").impl(unpack(args.fargs))
end, "*")
-- :GoInstall
cmd("GoInstallDeps", function()
require("gopher").install_deps()
end)
cmd("GoInstallDepsSync", function()
require("gopher").install_deps { sync = true }
end)
-- :GoTag
cmd("GoTagAdd", function(opts)
require("gopher").tags.add(unpack(opts.fargs))
end, "*")
cmd("GoTagRm", function(opts)
require("gopher").tags.rm(unpack(opts.fargs))
end, "*")
cmd("GoTagClear", function()
require("gopher").tags.clear()
end)
-- :GoTest
cmd("GoTestAdd", function()
require("gopher").test.add()
end)
cmd("GoTestsAll", function()
require("gopher").test.all()
end)
cmd("GoTestsExp", function()
require("gopher").test.exported()
end)
-- :Go
cmd("GoMod", function(opts)
require("gopher").mod(opts.fargs)
end, "*")
cmd("GoGet", function(opts)
vim.print(opts)
require("gopher").get(opts.fargs)
end, "*")
cmd("GoWork", function(opts)
require("gopher").get(opts.fargs)
end, "*")
cmd("GoGenerate", function(opts)
require("gopher").generate(opts.fargs or "")
end, "?")

View file

@ -1,14 +0,0 @@
command! -nargs=* GoTagAdd :lua require"gopher".tags.add(<f-args>)
command! -nargs=* GoTagRm :lua require"gopher".tags.rm(<f-args>)
command! GoTestAdd :lua require"gopher".test.add()
command! GoTestsAll :lua require"gopher".test.all()
command! GoTestsExp :lua require"gopher".test.exported()
command! -nargs=* GoMod :lua require"gopher".mod(<f-args>)
command! -nargs=* GoGet :lua require"gopher".get(<f-args>)
command! -nargs=* GoWork :lua require"gopher".work(<f-args>)
command! -nargs=* GoImpl :lua require"gopher".impl(<f-args>)
command! -nargs=* GoGenerate :lua require"gopher".generate(<f-args>)
command! GoCmt :lua require"gopher".comment()
command! GoIfErr :lua require"gopher".iferr()
command! GoInstallDeps :lua require"gopher".install_deps()
command! GopherLog :lua vim.cmd("tabnew " .. require("gopher._utils.log").get_outfile())

View file

@ -10,12 +10,12 @@ end
local files = {
"lua/gopher/init.lua",
"lua/gopher/config.lua",
"plugin/gopher.lua",
"lua/gopher/struct_tags.lua",
"lua/gopher/impl.lua",
"lua/gopher/gotests.lua",
"lua/gopher/iferr.lua",
"lua/gopher/comment.lua",
"lua/gopher/dap.lua",
}
minidoc.setup()
@ -30,4 +30,8 @@ hooks.write_pre = function(lines)
return lines
end
hooks.sections["@dochide"] = function(s)
s.parent:clear_lines()
end
MiniDoc.generate(files, "doc/gopher.nvim.txt", { hooks = hooks })

View file

@ -6,29 +6,58 @@ end
local function install_plug(plugin)
local name = plugin:match ".*/(.*)"
local package_root = root ".tests/site/pack/deps/start/"
if not vim.loop.fs_stat(package_root .. name) then
if not vim.uv.fs_stat(package_root .. name) then
print("Installing " .. plugin)
vim.fn.mkdir(package_root, "p")
vim.fn.system {
vim
.system({
"git",
"clone",
"--depth=1",
"https://github.com/" .. plugin .. ".git",
package_root .. "/" .. name,
}
})
:wait()
end
end
vim.cmd [[set runtimepath=$VIMRUNTIME]]
vim.opt.runtimepath:append(root())
vim.opt.packpath = { root ".tests/site" }
vim.notify = print
install_plug "nvim-lua/plenary.nvim"
install_plug "nvim-treesitter/nvim-treesitter"
install_plug "echasnovski/mini.doc" -- used for docs generation
install_plug "echasnovski/mini.test"
vim.env.XDG_CONFIG_HOME = root ".tests/config"
vim.env.XDG_DATA_HOME = root ".tests/data"
vim.env.XDG_STATE_HOME = root ".tests/state"
vim.env.XDG_CACHE_HOME = root ".tests/cache"
vim.opt.runtimepath:append(root())
vim.opt.packpath:append(root ".tests/site")
vim.notify = vim.print
-- install go treesitter parse
require("nvim-treesitter.install").ensure_installed_sync "go"
require("gopher").setup {
log_level = vim.log.levels.OFF,
timeout = 4000,
}
-- setup mini.test only when running headless nvim
if #vim.api.nvim_list_uis() == 0 then
require("mini.test").setup {
collect = {
find_files = function()
return vim.fn.globpath("spec", "**/*_test.lua", true, true)
end,
},
}
end
-- needed for tests, i dont know the reason why, but on start
-- vim is not able to use treesitter for go by default
vim.api.nvim_create_autocmd("FileType", {
pattern = "go",
callback = function(args)
vim.treesitter.start(args.buf, "go")
end,
})

0
spec/fixtures/comment/empty_input.go vendored Normal file
View file

2
spec/fixtures/comment/empty_output.go vendored Normal file
View file

@ -0,0 +1,2 @@
//

5
spec/fixtures/comment/func_input.go vendored Normal file
View file

@ -0,0 +1,5 @@
package main
func Test(a int) bool {
return false
}

6
spec/fixtures/comment/func_output.go vendored Normal file
View file

@ -0,0 +1,6 @@
package main
// Test
func Test(a int) bool {
return false
}

View file

@ -0,0 +1,3 @@
package main
type Testinger interface{}

View file

@ -0,0 +1,4 @@
package main
// Testinger
type Testinger interface{}

7
spec/fixtures/comment/method_input.go vendored Normal file
View file

@ -0,0 +1,7 @@
package main
type Method struct{}
func (Method) Run() error {
return nil
}

View file

@ -0,0 +1,8 @@
package main
type Method struct{}
// Run
func (Method) Run() error {
return nil
}

View file

@ -0,0 +1 @@
package main

View file

@ -0,0 +1,2 @@
// Package main provides
package main

3
spec/fixtures/comment/struct_input.go vendored Normal file
View file

@ -0,0 +1,3 @@
package main
type CommentStruct struct{}

View file

@ -0,0 +1,4 @@
package main
// CommentStruct
type CommentStruct struct{}

9
spec/fixtures/iferr/iferr_input.go vendored Normal file
View file

@ -0,0 +1,9 @@
package main
func test() error {
return nil
}
func main() {
err := test()
}

12
spec/fixtures/iferr/iferr_output.go vendored Normal file
View file

@ -0,0 +1,12 @@
package main
func test() error {
return nil
}
func main() {
err := test()
if err != nil {
return
}
}

7
spec/fixtures/iferr/message_input.go vendored Normal file
View file

@ -0,0 +1,7 @@
package main
func getErr() error { return nil }
func test() error {
err := getErr()
}

10
spec/fixtures/iferr/message_output.go vendored Normal file
View file

@ -0,0 +1,10 @@
package main
func getErr() error { return nil }
func test() error {
err := getErr()
if err != nil {
return fmt.Errorf("failed to %w", err)
}
}

3
spec/fixtures/impl/closer_input.go vendored Normal file
View file

@ -0,0 +1,3 @@
package main
type CloserTest struct{}

7
spec/fixtures/impl/closer_output.go vendored Normal file
View file

@ -0,0 +1,7 @@
package main
type CloserTest2 struct{}
func (closertest *CloserTest2) Close() error {
panic("not implemented") // TODO: Implement
}

3
spec/fixtures/impl/reader_input.go vendored Normal file
View file

@ -0,0 +1,3 @@
package main
type Read struct{}

7
spec/fixtures/impl/reader_output.go vendored Normal file
View file

@ -0,0 +1,7 @@
package main
func (r *Read2) Read(p []byte) (n int, err error) {
panic("not implemented") // TODO: Implement
}
type Read2 struct{}

3
spec/fixtures/impl/writer_input.go vendored Normal file
View file

@ -0,0 +1,3 @@
package main
type WriterTest struct{}

7
spec/fixtures/impl/writer_output.go vendored Normal file
View file

@ -0,0 +1,7 @@
package main
type WriterTest2 struct{}
func (w *WriterTest2) Write(p []byte) (n int, err error) {
panic("not implemented") // TODO: Implement
}

11
spec/fixtures/tags/add_many_input.go vendored Normal file
View file

@ -0,0 +1,11 @@
package main
type Test struct {
ID int
Name string
Num int64
Another struct {
First int
Second string
}
}

11
spec/fixtures/tags/add_many_output.go vendored Normal file
View file

@ -0,0 +1,11 @@
package main
type Test struct {
ID int `test4:"id" test5:"id" test1:"id" test2:"id"`
Name string `test4:"name" test5:"name" test1:"name" test2:"name"`
Num int64 `test4:"num" test5:"num" test1:"num" test2:"num"`
Another struct {
First int `test4:"first" test5:"first" test1:"first" test2:"first"`
Second string `test4:"second" test5:"second" test1:"second" test2:"second"`
} `test4:"another" test5:"another" test1:"another" test2:"another"`
}

11
spec/fixtures/tags/clear_input.go vendored Normal file
View file

@ -0,0 +1,11 @@
package main
type Test struct {
ID int `json:"id" yaml:"id" xml:"id" db:"id"`
Name string `json:"name" yaml:"name" xml:"name" db:"name"`
Num int64 `json:"num" yaml:"num" xml:"num" db:"num"`
Another struct {
First int `json:"first" yaml:"first" xml:"first" db:"first"`
Second string `json:"second" yaml:"second" xml:"second" db:"second"`
} `json:"another" yaml:"another" xml:"another" db:"another"`
}

11
spec/fixtures/tags/clear_output.go vendored Normal file
View file

@ -0,0 +1,11 @@
package main
type Test struct {
ID int
Name string
Num int64
Another struct {
First int
Second string
}
}

18
spec/fixtures/tags/many_input.go vendored Normal file
View file

@ -0,0 +1,18 @@
package main
type (
TestOne struct {
Asdf string
ID int
}
TestTwo struct {
Fesa int
A bool
}
TestThree struct {
Asufj int
Fs string
}
)

18
spec/fixtures/tags/many_output.go vendored Normal file
View file

@ -0,0 +1,18 @@
package main
type (
TestOne struct {
Asdf string
ID int
}
TestTwo struct {
Fesa int `testing:"fesa"`
A bool `testing:"a"`
}
TestThree struct {
Asufj int
Fs string
}
)

11
spec/fixtures/tags/svar_input.go vendored Normal file
View file

@ -0,0 +1,11 @@
package main
func main() {
s := struct {
API string
Key string
}{
API: "api.com",
Key: "key",
}
}

11
spec/fixtures/tags/svar_output.go vendored Normal file
View file

@ -0,0 +1,11 @@
package main
func main() {
s := struct {
API string `xml:"api"`
Key string `xml:"key"`
}{
API: "api.com",
Key: "key",
}
}

8
spec/fixtures/tags/var_input.go vendored Normal file
View file

@ -0,0 +1,8 @@
package main
func main() {
var a struct {
TestField1 string
TestField2 string
}
}

8
spec/fixtures/tags/var_output.go vendored Normal file
View file

@ -0,0 +1,8 @@
package main
func main() {
var a struct {
TestField1 string `yaml:"test_field_1"`
TestField2 string `yaml:"test_field_2"`
}
}

5
spec/fixtures/tests/function_input.go vendored Normal file
View file

@ -0,0 +1,5 @@
package fortest
func Add(x, y int) int {
return 2 + x + y
}

24
spec/fixtures/tests/function_output.go vendored Normal file
View file

@ -0,0 +1,24 @@
package fortest
import "testing"
func TestAdd(t *testing.T) {
type args struct {
x int
y int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.args.x, tt.args.y); got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}

7
spec/fixtures/tests/method_input.go vendored Normal file
View file

@ -0,0 +1,7 @@
package fortest
type ForTest struct{}
func (t *ForTest) Add(x, y int) int {
return 2 + x + y
}

26
spec/fixtures/tests/method_output.go vendored Normal file
View file

@ -0,0 +1,26 @@
package fortest
import "testing"
func TestForTest_Add(t *testing.T) {
type args struct {
x int
y int
}
tests := []struct {
name string
tr *ForTest
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tr := &ForTest{}
if got := tr.Add(tt.args.x, tt.args.y); got != tt.want {
t.Errorf("ForTest.Add() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -0,0 +1,37 @@
local t = require "spec.testutils"
local child, T = t.setup "comment"
local function do_the_test(fixture, pos)
local rs = t.setup_test("comment/" .. fixture, child, pos)
child.cmd "GoCmt"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["comment"]["should add comment to package"] = function()
do_the_test("package", { 1, 1 })
end
T["comment"]["should add comment to struct"] = function()
do_the_test("struct", { 4, 1 })
end
T["comment"]["should add comment to function"] = function()
do_the_test("func", { 3, 1 })
end
T["comment"]["should add comment to method"] = function()
do_the_test("method", { 5, 1 })
end
T["comment"]["should add comment to interface"] = function()
do_the_test("interface", { 3, 6 })
end
T["comment"]["otherwise should add // above cursor"] = function()
do_the_test("empty", { 1, 1 })
end
return T

View file

@ -0,0 +1,29 @@
local t = require "spec.testutils"
local child, T = t.setup "gotests"
--- NOTE: :GoTestAdd is the only place that has actual logic
--- All other parts are handled `gotests` itself.
---@param fpath string
---@return string
local function read_testfile(fpath)
return t.readfile(fpath:gsub(".go", "_test.go"))
end
T["gotests"]["should add test for function under cursor"] = function()
local rs = t.setup_test("tests/function", child, { 3, 5 })
child.cmd "GoTestAdd"
t.eq(rs.fixtures.output, read_testfile(rs.tmp))
t.cleanup(rs)
end
T["gotests"]["should add test for method under cursor"] = function()
local rs = t.setup_test("tests/method", child, { 5, 19 })
child.cmd "GoTestAdd"
t.eq(rs.fixtures.output, read_testfile(rs.tmp))
t.cleanup(rs)
end
return T

View file

@ -0,0 +1,27 @@
local t = require "spec.testutils"
local child, T = t.setup "iferr"
T["iferr"]["should add if != nil {"] = function()
local rs = t.setup_test("iferr/iferr", child, { 8, 2 })
child.cmd "GoIfErr"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["iferr"]["should add if err with custom message"] = function()
child.lua [[
require("gopher").setup {
iferr = { message = 'fmt.Errorf("failed to %w", err)' }
} ]]
local rs = t.setup_test("iferr/message", child, { 6, 2 })
child.cmd "GoIfErr"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
return T

View file

@ -0,0 +1,35 @@
local t = require "spec.testutils"
local child, T = t.setup "impl"
T["impl"]["should do impl with 'w io.Writer'"] = function()
local rs = t.setup_test("impl/writer", child, { 3, 0 })
child.cmd "GoImpl w io.Writer"
child.cmd "write"
-- NOTE: since "impl" won't implement interface if it's already implemented i went with this hack
local rhs = rs.fixtures.output:gsub("Test2", "Test")
t.eq(t.readfile(rs.tmp), rhs)
t.cleanup(rs)
end
T["impl"]["should work with full input, 'r Read io.Reader'"] = function()
local rs = t.setup_test("impl/reader", child)
child.cmd "GoImpl r Read io.Reader"
child.cmd "write"
local rhs = rs.fixtures.output:gsub("Read2", "Read")
t.eq(t.readfile(rs.tmp), rhs)
t.cleanup(rs)
end
T["impl"]["should work with minimal input 'io.Closer'"] = function()
local rs = t.setup_test("impl/closer", child, { 3, 6 })
child.cmd "GoImpl io.Closer"
child.cmd "write"
local rhs = rs.fixtures.output:gsub("Test2", "Test")
t.eq(t.readfile(rs.tmp), rhs)
t.cleanup(rs)
end
return T

View file

@ -0,0 +1,81 @@
local t = require "spec.testutils"
local child, T = t.setup "struct_tags"
T["struct_tags"]["should add tag"] = function()
local rs = t.setup_test("tags/add", child, { 3, 6 })
child.cmd "GoTagAdd json"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["struct_tags"]["should remove tag"] = function()
local rs = t.setup_test("tags/remove", child, { 4, 6 })
child.cmd "GoTagRm json"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["struct_tags"]["should be able to handle many structs"] = function()
local rs = t.setup_test("tags/many", child, { 10, 3 })
child.cmd "GoTagAdd testing"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["struct_tags"]["should clear struct"] = function()
local rs = t.setup_test("tags/clear", child, { 3, 1 })
child.cmd "GoTagClear"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["struct_tags"]["should add more than one tag"] = function()
local tmp = t.tmpfile()
local fixtures = t.get_fixtures "tags/add_many"
t.writefile(tmp, fixtures.input)
--- with comma, like gomodifytags
child.cmd("silent edit " .. tmp)
child.fn.setpos(".", { child.fn.bufnr(tmp), 3, 1 })
child.cmd "GoTagAdd test4,test5"
child.cmd "write"
-- without comma
child.cmd("silent edit " .. tmp)
child.fn.setpos(".", { child.fn.bufnr(tmp), 3, 1 })
child.cmd "GoTagAdd test1 test2"
child.cmd "write"
t.eq(t.readfile(tmp), fixtures.output)
---@diagnostic disable-next-line:missing-fields
t.cleanup { tmp = tmp }
end
T["struct_tags"]["should add tags on var"] = function()
local rs = t.setup_test("tags/var", child, { 5, 6 })
child.cmd "GoTagAdd yaml"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
T["struct_tags"]["should add tags on short declr var"] = function()
local rs = t.setup_test("tags/svar", child, { 4, 3 })
child.cmd "GoTagAdd xml"
child.cmd "write"
t.eq(t.readfile(rs.tmp), rs.fixtures.output)
t.cleanup(rs)
end
return T

102
spec/testutils.lua Normal file
View file

@ -0,0 +1,102 @@
local base_dir = vim.env.GOPHER_DIR or vim.fn.expand "%:p:h"
---@class gopher.TestUtils
local testutils = {}
testutils.mininit_path = vim.fs.joinpath(base_dir, "scripts", "minimal_init.lua")
testutils.fixtures_dir = vim.fs.joinpath(base_dir, "spec/fixtures")
---@param name string
---@return MiniTest.child, table
function testutils.setup(name)
local child = MiniTest.new_child_neovim()
local T = MiniTest.new_set {
hooks = {
post_once = child.stop,
pre_case = function()
child.restart { "-u", testutils.mininit_path }
end,
},
}
T[name] = MiniTest.new_set {}
return child, T
end
---@generic T
---@param a T
---@param b T
---@return boolean
function testutils.eq(a, b)
return MiniTest.expect.equality(a, b)
end
---@return string
function testutils.tmpfile()
return vim.fn.tempname() .. ".go"
end
---@param path string
---@return string
function testutils.readfile(path)
return vim.fn.join(vim.fn.readfile(path), "\n")
end
---@param fpath string
---@param contents string
function testutils.writefile(fpath, contents)
vim.fn.writefile(vim.split(contents, "\n"), fpath)
end
---@param fpath string
function testutils.deletefile(fpath)
vim.fn.delete(fpath)
end
---@class gopher.TestUtilsFixtures
---@field input string
---@field output string
---@param fixture string
---@return gopher.TestUtilsFixtures
function testutils.get_fixtures(fixture)
return {
input = testutils.readfile(vim.fs.joinpath(testutils.fixtures_dir, fixture) .. "_input.go"),
output = testutils.readfile(vim.fs.joinpath(testutils.fixtures_dir, fixture) .. "_output.go"),
}
end
---@class gopher.TestUtilsSetup
---@field tmp string
---@field fixtures gopher.TestUtilsFixtures
---@field bufnr number
---@param fixture string
---@param child MiniTest.child
---@param pos? number[]
---@return gopher.TestUtilsSetup
function testutils.setup_test(fixture, child, pos)
local tmp = testutils.tmpfile()
local fixtures = testutils.get_fixtures(fixture)
testutils.writefile(tmp, fixtures.input)
child.cmd("silent edit " .. tmp)
local bufnr = child.fn.bufnr(tmp)
if pos then
child.fn.setpos(".", { bufnr, unpack(pos) })
end
return {
tmp = tmp,
bufnr = bufnr,
fixtures = fixtures,
}
end
---@param inp gopher.TestUtilsSetup
function testutils.cleanup(inp)
testutils.deletefile(inp.tmp)
end
return testutils

25
spec/unit/utils_test.lua Normal file
View file

@ -0,0 +1,25 @@
local t = require "spec.testutils"
local _, T = t.setup "utils"
T["utils"]["should .remove_empty_lines()"] = function()
local u = require "gopher._utils"
local inp = { "hi", "", "a", "", "", "asdf" }
t.eq(u.remove_empty_lines(inp), { "hi", "a", "asdf" })
end
T["utils"]["should .readfile_joined()"] = function()
local data = "line1\nline2\nline3"
local tmp = t.tmpfile()
local u = require "gopher._utils"
t.writefile(tmp, data)
t.eq(u.readfile_joined(tmp), data)
end
T["utils"]["should .trimend()"] = function()
local u = require "gopher._utils"
t.eq(u.trimend " hi ", " hi")
end
return T

View file

@ -1,29 +0,0 @@
describe("gopher.config", function()
it(".setup() should provide default when .setup() is not called", function()
local c = require "gopher.config"
assert.are.same(c.commands.go, "go")
assert.are.same(c.commands.gomodifytags, "gomodifytags")
assert.are.same(c.commands.gotests, "gotests")
assert.are.same(c.commands.impl, "impl")
assert.are.same(c.commands.iferr, "iferr")
assert.are.same(c.commands.dlv, "dlv")
end)
it(".setup() should change options on users config", function()
local c = require "gopher.config"
c.setup {
commands = {
go = "go1.420",
gomodifytags = "iDontUseRustBtw",
},
}
assert.are.same(c.commands.go, "go1.420")
assert.are.same(c.commands.gomodifytags, "iDontUseRustBtw")
assert.are.same(c.commands.gotests, "gotests")
assert.are.same(c.commands.impl, "impl")
assert.are.same(c.commands.iferr, "iferr")
assert.are.same(c.commands.dlv, "dlv")
end)
end)

View file

@ -1,15 +0,0 @@
describe("gopher._utils", function()
local u = require "gopher._utils"
describe(".sreq()", function()
it("can require existing module", function()
assert.are.same(require "gopher", u.sreq "gopher")
end)
it("cannot require non-existing module", function()
assert.has.errors(function()
u.sreq "iDontExistBtw"
end)
end)
end)
end)