diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..1973517 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +dotenv + +# GOPHER_DIR - needed only for tests, to find the root of the project +env_vars_required GOPHER_DIR diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index a78c3c3..0122b93 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -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 " ") diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b16b5d..103b18e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.gitignore b/.gitignore index df7d859..3a9d44e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /playground/ /.tests/ +/.env diff --git a/.luarc.json b/.luarc.json deleted file mode 100644 index a0d5712..0000000 --- a/.luarc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "diagnostics.globals": [ - "describe", - "it", - "before_each", - "after_each", - "before_all", - "after_all" - ] -} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 39092b1..ef377e2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 ``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8596ec0 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md index d256e5f..acb0554 100644 --- a/README.md +++ b/README.md @@ -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)
@@ -215,20 +222,6 @@ require("gopher").setup { ```
-
- - Setup nvim-dap for go in one line - - - 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() - ``` -
- ## 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) diff --git a/Taskfile.yml b/Taskfile.yml index 581f11c..19cbc8f 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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 \ - -u ./scripts/minimal_init.lua \ - -c "PlenaryBustedDirectory spec \ - {minimal_init='./scripts/minimal_init.lua' \ - ,sequential=true}" \ - -c ":qa!" + nvim --clean --headless \ + -u ./scripts/minimal_init.lua \ + -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!" diff --git a/autoload/health/gopher.vim b/autoload/health/gopher.vim deleted file mode 100644 index 4e3c56d..0000000 --- a/autoload/health/gopher.vim +++ /dev/null @@ -1,3 +0,0 @@ -function! health#gopher#check() - lua require("gopher.health").check() -endfunction diff --git a/doc/gopher.nvim.txt b/doc/gopher.nvim.txt index d4a82d0..47aa005 100644 --- a/doc/gopher.nvim.txt +++ b/doc/gopher.nvim.txt @@ -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. +To install these tools, you can run `:GoInstallDeps` command +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 @@ -143,78 +170,51 @@ simple example: // this is what you will get func (b *BytesReader) Read(p []byte) (n int, err error) { - panic("not implemented") // TODO: Implement + panic("not implemented") // TODO: Implement } < - ============================================================================== ------------------------------------------------------------------------------ *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: \ No newline at end of file diff --git a/lua/gopher/_utils/runner/gocmd.lua b/lua/gopher/_utils/gocmd.lua similarity index 70% rename from lua/gopher/_utils/runner/gocmd.lua rename to lua/gopher/_utils/gocmd.lua index 97323f9..a091c4f 100644 --- a/lua/gopher/_utils/runner/gocmd.lua +++ b/lua/gopher/_utils/gocmd.lua @@ -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) - end - u.notify(c.go .. " " .. subcmd .. " successful runned") - end, - }) + 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 .. " ran successful") + return rs.stdout end return gocmd diff --git a/lua/gopher/_utils/health_util.lua b/lua/gopher/_utils/health_util.lua deleted file mode 100644 index e1d44ee..0000000 --- a/lua/gopher/_utils/health_util.lua +++ /dev/null @@ -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 diff --git a/lua/gopher/_utils/init.lua b/lua/gopher/_utils/init.lua index a3b567c..1a0ce75 100644 --- a/lua/gopher/_utils/init.lua +++ b/lua/gopher/_utils/init.lua @@ -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 diff --git a/lua/gopher/_utils/log.lua b/lua/gopher/_utils/log.lua index c7dccd7..1c77369 100644 --- a/lua/gopher/_utils/log.lua +++ b/lua/gopher/_utils/log.lua @@ -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 diff --git a/lua/gopher/_utils/runner.lua b/lua/gopher/_utils/runner.lua new file mode 100644 index 0000000..15eaa42 --- /dev/null +++ b/lua/gopher/_utils/runner.lua @@ -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 diff --git a/lua/gopher/_utils/runner/init.lua b/lua/gopher/_utils/runner/init.lua deleted file mode 100644 index 3c08f7f..0000000 --- a/lua/gopher/_utils/runner/init.lua +++ /dev/null @@ -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 diff --git a/lua/gopher/_utils/ts.lua b/lua/gopher/_utils/ts.lua new file mode 100644 index 0000000..29d38fe --- /dev/null +++ b/lua/gopher/_utils/ts.lua @@ -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 diff --git a/lua/gopher/_utils/ts/init.lua b/lua/gopher/_utils/ts/init.lua deleted file mode 100644 index aba3a94..0000000 --- a/lua/gopher/_utils/ts/init.lua +++ /dev/null @@ -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 diff --git a/lua/gopher/_utils/ts/nodes.lua b/lua/gopher/_utils/ts/nodes.lua deleted file mode 100644 index e9c730d..0000000 --- a/lua/gopher/_utils/ts/nodes.lua +++ /dev/null @@ -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 diff --git a/lua/gopher/comment.lua b/lua/gopher/comment.lua index 8754405..5a91ed1 100644 --- a/lua/gopher/comment.lua +++ b/lua/gopher/comment.lua @@ -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 diff --git a/lua/gopher/config.lua b/lua/gopher/config.lua index 9c20843..815b14b 100644 --- a/lua/gopher/config.lua +++ b/lua/gopher/config.lua @@ -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 diff --git a/lua/gopher/dap.lua b/lua/gopher/dap.lua deleted file mode 100644 index 9930318..0000000 --- a/lua/gopher/dap.lua +++ /dev/null @@ -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 diff --git a/lua/gopher/gotests.lua b/lua/gopher/gotests.lua index da4753d..d896864 100644 --- a/lua/gopher/gotests.lua +++ b/lua/gopher/gotests.lua @@ -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) - end + 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, - }) + u.notify "unit test(s) generated" 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 diff --git a/lua/gopher/health.lua b/lua/gopher/health.lua index 633a184..f45c499 100644 --- a/lua/gopher/health.lua +++ b/lua/gopher/health.lua @@ -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 diff --git a/lua/gopher/iferr.lua b/lua/gopher/iferr.lua index bcd4b24..78d060f 100644 --- a/lua/gopher/iferr.lua +++ b/lua/gopher/iferr.lua @@ -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 diff --git a/lua/gopher/impl.lua b/lua/gopher/impl.lua index f461376..702d58e 100644 --- a/lua/gopher/impl.lua +++ b/lua/gopher/impl.lua @@ -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 @@ -22,7 +29,7 @@ --- --- // this is what you will get --- func (b *BytesReader) Read(p []byte) (n int, err error) { ---- panic("not implemented") // TODO: Implement +--- panic("not implemented") // TODO: Implement --- } --- < @@ -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) - end - end, - }) + 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 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 diff --git a/lua/gopher/init.lua b/lua/gopher/init.lua index 2aa138e..9afac0c 100644 --- a/lua/gopher/init.lua +++ b/lua/gopher/init.lua @@ -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. +--- To install these tools, you can run `:GoInstallDeps` command +--- 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 diff --git a/lua/gopher/installer.lua b/lua/gopher/installer.lua index 2994b8a..cc61f92 100644 --- a/lua/gopher/installer.lua +++ b/lua/gopher/installer.lua @@ -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) - return - end - u.notify("installed: " .. url) - end, - }) +---@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 + + 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 diff --git a/lua/gopher/struct_tags.lua b/lua/gopher/struct_tags.lua index 4389b62..cf990bf 100644 --- a/lua/gopher/struct_tags.lua +++ b/lua/gopher/struct_tags.lua @@ -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) - end - end, - }) + 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 - -- 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 diff --git a/nvim.toml b/nvim.toml index fa09a88..50dc11f 100644 --- a/nvim.toml +++ b/nvim.toml @@ -1,6 +1,9 @@ [vim] any = true +[MiniTest] +any = true + [describe] any = true [[describe.args]] diff --git a/pkg.json b/pkg.json new file mode 100644 index 0000000..4118802 --- /dev/null +++ b/pkg.json @@ -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": "*" + } +} diff --git a/plugin/gopher.lua b/plugin/gopher.lua new file mode 100644 index 0000000..1ccc74e --- /dev/null +++ b/plugin/gopher.lua @@ -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, "?") diff --git a/plugin/gopher.vim b/plugin/gopher.vim deleted file mode 100644 index a219a1c..0000000 --- a/plugin/gopher.vim +++ /dev/null @@ -1,14 +0,0 @@ -command! -nargs=* GoTagAdd :lua require"gopher".tags.add() -command! -nargs=* GoTagRm :lua require"gopher".tags.rm() -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() -command! -nargs=* GoGet :lua require"gopher".get() -command! -nargs=* GoWork :lua require"gopher".work() -command! -nargs=* GoImpl :lua require"gopher".impl() -command! -nargs=* GoGenerate :lua require"gopher".generate() -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()) diff --git a/scripts/docgen.lua b/scripts/docgen.lua index f2deb8b..1c66b90 100644 --- a/scripts/docgen.lua +++ b/scripts/docgen.lua @@ -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 }) diff --git a/scripts/minimal_init.lua b/scripts/minimal_init.lua index 49a606e..b0f51e6 100644 --- a/scripts/minimal_init.lua +++ b/scripts/minimal_init.lua @@ -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 { - "git", - "clone", - "--depth=1", - "https://github.com/" .. plugin .. ".git", - package_root .. "/" .. name, - } + 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, +}) diff --git a/spec/fixtures/comment/empty_input.go b/spec/fixtures/comment/empty_input.go new file mode 100644 index 0000000..e69de29 diff --git a/spec/fixtures/comment/empty_output.go b/spec/fixtures/comment/empty_output.go new file mode 100644 index 0000000..aa56d2c --- /dev/null +++ b/spec/fixtures/comment/empty_output.go @@ -0,0 +1,2 @@ +// + diff --git a/spec/fixtures/comment/func_input.go b/spec/fixtures/comment/func_input.go new file mode 100644 index 0000000..aec80b0 --- /dev/null +++ b/spec/fixtures/comment/func_input.go @@ -0,0 +1,5 @@ +package main + +func Test(a int) bool { + return false +} diff --git a/spec/fixtures/comment/func_output.go b/spec/fixtures/comment/func_output.go new file mode 100644 index 0000000..7dc39e5 --- /dev/null +++ b/spec/fixtures/comment/func_output.go @@ -0,0 +1,6 @@ +package main + +// Test +func Test(a int) bool { + return false +} diff --git a/spec/fixtures/comment/interface_input.go b/spec/fixtures/comment/interface_input.go new file mode 100644 index 0000000..c065125 --- /dev/null +++ b/spec/fixtures/comment/interface_input.go @@ -0,0 +1,3 @@ +package main + +type Testinger interface{} diff --git a/spec/fixtures/comment/interface_output.go b/spec/fixtures/comment/interface_output.go new file mode 100644 index 0000000..689bc6d --- /dev/null +++ b/spec/fixtures/comment/interface_output.go @@ -0,0 +1,4 @@ +package main + +// Testinger +type Testinger interface{} diff --git a/spec/fixtures/comment/method_input.go b/spec/fixtures/comment/method_input.go new file mode 100644 index 0000000..39f715c --- /dev/null +++ b/spec/fixtures/comment/method_input.go @@ -0,0 +1,7 @@ +package main + +type Method struct{} + +func (Method) Run() error { + return nil +} diff --git a/spec/fixtures/comment/method_output.go b/spec/fixtures/comment/method_output.go new file mode 100644 index 0000000..2ffdf87 --- /dev/null +++ b/spec/fixtures/comment/method_output.go @@ -0,0 +1,8 @@ +package main + +type Method struct{} + +// Run +func (Method) Run() error { + return nil +} diff --git a/spec/fixtures/comment/package_input.go b/spec/fixtures/comment/package_input.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/spec/fixtures/comment/package_input.go @@ -0,0 +1 @@ +package main diff --git a/spec/fixtures/comment/package_output.go b/spec/fixtures/comment/package_output.go new file mode 100644 index 0000000..3721c5c --- /dev/null +++ b/spec/fixtures/comment/package_output.go @@ -0,0 +1,2 @@ +// Package main provides +package main diff --git a/spec/fixtures/comment/struct_input.go b/spec/fixtures/comment/struct_input.go new file mode 100644 index 0000000..98e8561 --- /dev/null +++ b/spec/fixtures/comment/struct_input.go @@ -0,0 +1,3 @@ +package main + +type CommentStruct struct{} diff --git a/spec/fixtures/comment/struct_output.go b/spec/fixtures/comment/struct_output.go new file mode 100644 index 0000000..14e279d --- /dev/null +++ b/spec/fixtures/comment/struct_output.go @@ -0,0 +1,4 @@ +package main + +// CommentStruct +type CommentStruct struct{} diff --git a/spec/fixtures/iferr/iferr_input.go b/spec/fixtures/iferr/iferr_input.go new file mode 100644 index 0000000..b94f158 --- /dev/null +++ b/spec/fixtures/iferr/iferr_input.go @@ -0,0 +1,9 @@ +package main + +func test() error { + return nil +} + +func main() { + err := test() +} diff --git a/spec/fixtures/iferr/iferr_output.go b/spec/fixtures/iferr/iferr_output.go new file mode 100644 index 0000000..8ab181a --- /dev/null +++ b/spec/fixtures/iferr/iferr_output.go @@ -0,0 +1,12 @@ +package main + +func test() error { + return nil +} + +func main() { + err := test() + if err != nil { + return + } +} diff --git a/spec/fixtures/iferr/message_input.go b/spec/fixtures/iferr/message_input.go new file mode 100644 index 0000000..5998cba --- /dev/null +++ b/spec/fixtures/iferr/message_input.go @@ -0,0 +1,7 @@ +package main + +func getErr() error { return nil } + +func test() error { + err := getErr() +} diff --git a/spec/fixtures/iferr/message_output.go b/spec/fixtures/iferr/message_output.go new file mode 100644 index 0000000..7d10241 --- /dev/null +++ b/spec/fixtures/iferr/message_output.go @@ -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) + } +} diff --git a/spec/fixtures/impl/closer_input.go b/spec/fixtures/impl/closer_input.go new file mode 100644 index 0000000..e4d5f52 --- /dev/null +++ b/spec/fixtures/impl/closer_input.go @@ -0,0 +1,3 @@ +package main + +type CloserTest struct{} diff --git a/spec/fixtures/impl/closer_output.go b/spec/fixtures/impl/closer_output.go new file mode 100644 index 0000000..4f077f4 --- /dev/null +++ b/spec/fixtures/impl/closer_output.go @@ -0,0 +1,7 @@ +package main + +type CloserTest2 struct{} + +func (closertest *CloserTest2) Close() error { + panic("not implemented") // TODO: Implement +} diff --git a/spec/fixtures/impl/reader_input.go b/spec/fixtures/impl/reader_input.go new file mode 100644 index 0000000..ebc8eff --- /dev/null +++ b/spec/fixtures/impl/reader_input.go @@ -0,0 +1,3 @@ +package main + +type Read struct{} diff --git a/spec/fixtures/impl/reader_output.go b/spec/fixtures/impl/reader_output.go new file mode 100644 index 0000000..2f948c4 --- /dev/null +++ b/spec/fixtures/impl/reader_output.go @@ -0,0 +1,7 @@ +package main + +func (r *Read2) Read(p []byte) (n int, err error) { + panic("not implemented") // TODO: Implement +} + +type Read2 struct{} diff --git a/spec/fixtures/impl/writer_input.go b/spec/fixtures/impl/writer_input.go new file mode 100644 index 0000000..ef034cc --- /dev/null +++ b/spec/fixtures/impl/writer_input.go @@ -0,0 +1,3 @@ +package main + +type WriterTest struct{} diff --git a/spec/fixtures/impl/writer_output.go b/spec/fixtures/impl/writer_output.go new file mode 100644 index 0000000..b69a70c --- /dev/null +++ b/spec/fixtures/impl/writer_output.go @@ -0,0 +1,7 @@ +package main + +type WriterTest2 struct{} + +func (w *WriterTest2) Write(p []byte) (n int, err error) { + panic("not implemented") // TODO: Implement +} diff --git a/spec/fixtures/tags/add_many_input.go b/spec/fixtures/tags/add_many_input.go new file mode 100644 index 0000000..7e27a27 --- /dev/null +++ b/spec/fixtures/tags/add_many_input.go @@ -0,0 +1,11 @@ +package main + +type Test struct { + ID int + Name string + Num int64 + Another struct { + First int + Second string + } +} diff --git a/spec/fixtures/tags/add_many_output.go b/spec/fixtures/tags/add_many_output.go new file mode 100644 index 0000000..9a43e62 --- /dev/null +++ b/spec/fixtures/tags/add_many_output.go @@ -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"` +} diff --git a/spec/fixtures/tags/clear_input.go b/spec/fixtures/tags/clear_input.go new file mode 100644 index 0000000..050b327 --- /dev/null +++ b/spec/fixtures/tags/clear_input.go @@ -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"` +} diff --git a/spec/fixtures/tags/clear_output.go b/spec/fixtures/tags/clear_output.go new file mode 100644 index 0000000..7e27a27 --- /dev/null +++ b/spec/fixtures/tags/clear_output.go @@ -0,0 +1,11 @@ +package main + +type Test struct { + ID int + Name string + Num int64 + Another struct { + First int + Second string + } +} diff --git a/spec/fixtures/tags/many_input.go b/spec/fixtures/tags/many_input.go new file mode 100644 index 0000000..f5c6367 --- /dev/null +++ b/spec/fixtures/tags/many_input.go @@ -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 + } +) diff --git a/spec/fixtures/tags/many_output.go b/spec/fixtures/tags/many_output.go new file mode 100644 index 0000000..36877b8 --- /dev/null +++ b/spec/fixtures/tags/many_output.go @@ -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 + } +) diff --git a/spec/fixtures/tags/svar_input.go b/spec/fixtures/tags/svar_input.go new file mode 100644 index 0000000..7831d01 --- /dev/null +++ b/spec/fixtures/tags/svar_input.go @@ -0,0 +1,11 @@ +package main + +func main() { + s := struct { + API string + Key string + }{ + API: "api.com", + Key: "key", + } +} diff --git a/spec/fixtures/tags/svar_output.go b/spec/fixtures/tags/svar_output.go new file mode 100644 index 0000000..f320eb2 --- /dev/null +++ b/spec/fixtures/tags/svar_output.go @@ -0,0 +1,11 @@ +package main + +func main() { + s := struct { + API string `xml:"api"` + Key string `xml:"key"` + }{ + API: "api.com", + Key: "key", + } +} diff --git a/spec/fixtures/tags/var_input.go b/spec/fixtures/tags/var_input.go new file mode 100644 index 0000000..97b4bc3 --- /dev/null +++ b/spec/fixtures/tags/var_input.go @@ -0,0 +1,8 @@ +package main + +func main() { + var a struct { + TestField1 string + TestField2 string + } +} diff --git a/spec/fixtures/tags/var_output.go b/spec/fixtures/tags/var_output.go new file mode 100644 index 0000000..e158a3a --- /dev/null +++ b/spec/fixtures/tags/var_output.go @@ -0,0 +1,8 @@ +package main + +func main() { + var a struct { + TestField1 string `yaml:"test_field_1"` + TestField2 string `yaml:"test_field_2"` + } +} diff --git a/spec/fixtures/tests/function_input.go b/spec/fixtures/tests/function_input.go new file mode 100644 index 0000000..bde08df --- /dev/null +++ b/spec/fixtures/tests/function_input.go @@ -0,0 +1,5 @@ +package fortest + +func Add(x, y int) int { + return 2 + x + y +} diff --git a/spec/fixtures/tests/function_output.go b/spec/fixtures/tests/function_output.go new file mode 100644 index 0000000..42ebd4c --- /dev/null +++ b/spec/fixtures/tests/function_output.go @@ -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) + } + }) + } +} diff --git a/spec/fixtures/tests/method_input.go b/spec/fixtures/tests/method_input.go new file mode 100644 index 0000000..fe04124 --- /dev/null +++ b/spec/fixtures/tests/method_input.go @@ -0,0 +1,7 @@ +package fortest + +type ForTest struct{} + +func (t *ForTest) Add(x, y int) int { + return 2 + x + y +} diff --git a/spec/fixtures/tests/method_output.go b/spec/fixtures/tests/method_output.go new file mode 100644 index 0000000..7f927b5 --- /dev/null +++ b/spec/fixtures/tests/method_output.go @@ -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) + } + }) + } +} diff --git a/spec/integration/comment_test.lua b/spec/integration/comment_test.lua new file mode 100644 index 0000000..38e7909 --- /dev/null +++ b/spec/integration/comment_test.lua @@ -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 diff --git a/spec/integration/gotests_test.lua b/spec/integration/gotests_test.lua new file mode 100644 index 0000000..fcba8a7 --- /dev/null +++ b/spec/integration/gotests_test.lua @@ -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 diff --git a/spec/integration/iferr_test.lua b/spec/integration/iferr_test.lua new file mode 100644 index 0000000..fff53ba --- /dev/null +++ b/spec/integration/iferr_test.lua @@ -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 diff --git a/spec/integration/impl_test.lua b/spec/integration/impl_test.lua new file mode 100644 index 0000000..602d2b1 --- /dev/null +++ b/spec/integration/impl_test.lua @@ -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 diff --git a/spec/integration/struct_tags_test.lua b/spec/integration/struct_tags_test.lua new file mode 100644 index 0000000..1c97001 --- /dev/null +++ b/spec/integration/struct_tags_test.lua @@ -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 diff --git a/spec/testutils.lua b/spec/testutils.lua new file mode 100644 index 0000000..b359b4b --- /dev/null +++ b/spec/testutils.lua @@ -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 diff --git a/spec/unit/utils_test.lua b/spec/unit/utils_test.lua new file mode 100644 index 0000000..4be8126 --- /dev/null +++ b/spec/unit/utils_test.lua @@ -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 diff --git a/spec/units/config_spec.lua b/spec/units/config_spec.lua deleted file mode 100644 index 1fd91dd..0000000 --- a/spec/units/config_spec.lua +++ /dev/null @@ -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) diff --git a/spec/units/utils_spec.lua b/spec/units/utils_spec.lua deleted file mode 100644 index ea2f30c..0000000 --- a/spec/units/utils_spec.lua +++ /dev/null @@ -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)