diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..81eca2d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[*.{md,yml,yaml,toml,lua,vim}] +indent_size = 2 + +[*.go] +indent_style = tab +indent_size = 4 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/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 7b78986..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Format and lint -on: [push, pull_request] - -jobs: - format: - name: stylua - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: JohnnyMorganz/stylua-action@1.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --check . - - lint: - name: selene - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: NTBBloodbath/selene-action@v1.0.0 - with: - token: ${{ secrets.GITHUB_TOKEN }} - args: --display-style=quiet . diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..0122b93 --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,58 @@ +name: linters + +on: + push: + branches: + - main + - develop + pull_request: + +jobs: + linters: + name: Lua + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: JohnnyMorganz/stylua-action@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + version: latest + args: --check . + + - uses: NTBBloodbath/selene-action@v1.0.0 + 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 new file mode 100644 index 0000000..6fef69c --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,62 @@ +name: tests + +on: + push: + branches: + - main + - develop + pull_request: + +jobs: + tests: + strategy: + matrix: + os: [ubuntu-latest] + version: + - stable + - nightly + - v0.10.0 + - v0.10.4 + - v0.11.0 + - v0.11.1 + - v0.11.2 + - v0.11.3 + - v0.11.4 + runs-on: ${{ matrix.os }} + steps: + - name: Install Task + uses: arduino/setup-task@v1 + with: + version: 3.x + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.24.0" + check-latest: false + + - 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: task install-deps + + - name: Run Tests + run: | + nvim --version + task test diff --git a/.gitignore b/.gitignore index a5c9323..3a9d44e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -playground/ +/playground/ +/.tests/ +/.env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml deleted file mode 100644 index 464c44d..0000000 --- a/.pre-commit-config.yaml +++ /dev/null @@ -1,15 +0,0 @@ -repos: -- repo: local - hooks: - - id: stylua - name: StyLua - language: rust - entry: stylua - types: [lua] - args: ["--check", "-"] - - id: selene - name: Selene - language: rust - entry: selene - types: [lua] - args: ["-"] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..554e358 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,49 @@ +# Contributing to `gopher.nvim` +Thank you for taking the time to submit some code to gopher.nvim. It means a lot! + +### Task running +In this codebase for running tasks is used [Taskfile](https://taskfile.dev). +You can install it with: +```bash +go install github.com/go-task/task/v3/cmd/task@latest +``` + +### Formatting and linting +Code is formatted by [stylua](https://github.com/JohnnyMorganz/StyLua) and linted using [selene](https://github.com/Kampfkarren/selene). +You can install these with: + +```bash +sudo pacman -S selene stylua +# or whatever is your package manager +``` + +For formatting use this following commands, or setup your editor to integrate with selene/stylua: +```bash +task format +task lint +``` + +### Documentation +Here we're using [mini.doc](https://github.com/echasnovski/mini.nvim/blob/main/readmes/mini-doc.md) +for generating vimhelp files based on [LuaCats](https://luals.github.io/wiki/annotations/) annotations in comments. + +For demo gifs in [readme](./README.md) we're using [vhs](https://github.com/charmbracelet/vhs). +All files related to demos live in [/vhs](./vhs) dir. + +You can generate docs with: +```bash +task docgen # generates vimhelp +task vhs:generate # generates demo gifs +``` + +### Commit messages +We use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), please follow it. + +### Testing +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 +``` 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/Makefile b/Makefile deleted file mode 100644 index 9b270c1..0000000 --- a/Makefile +++ /dev/null @@ -1,11 +0,0 @@ -.PHONY: -.SILENT: - -format: - stylua **/*.lua - -lint: - selene **/*.lua - -test: - nvim --headless -u ./spec/minimal.vim -c "PlenaryBustedDirectory spec {minimal_init='./spec/minimal.vim'}" diff --git a/README.md b/README.md index 36d1d8b..44fcfb7 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,296 @@ # gopher.nvim +[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct-single.svg)](https://stand-with-ukraine.pp.ua) + 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. +It's **NOT** an LSP tool, the goal of this plugin is to add go tooling support in Neovim. -## Install +> All development of new and maybe undocumented, and unstable features is happening on [develop](https://github.com/olexsmir/gopher.nvim/tree/develop) branch. -Pre-dependency: [go](https://github.com/golang/go) (tested on 1.17 and 1.18) +## Table of content +* [How to install](#install-using-lazynvim) +* [Features](#features) +* [Configuration](#configuration) +* [Troubleshooting](#troubleshooting) +* [Contributing](#contributing) + +## Install (using [lazy.nvim](https://github.com/folke/lazy.nvim)) + +Requirements: + +- **Neovim 0.10** or later +- Treesitter parser for `go`(`:TSInstall go` if you use [nvim-treesitter](https://github.com/nvim-treesitter/nvim-treesitter)) +- [Go](https://github.com/golang/go) installed + +> [!IMPORTANT] +> If you prefer using other forges, this repository is also mirrored at: +> - [tangled.org](https://tangled.org): [`https://tangled.org/olexsmir.xyz/gopher.nvim`](https://tangled.org/olexsmir.xyz/gopher.nvim) +> - [codeberg.org](https://codeberg.org): [`https://codeberg.org/olexsmir/gopher.nvim`](https://codeberg.org/olexsmir/gopher.nvim) ```lua -use { +-- NOTE: this plugin is already lazy-loaded and adds only about 1ms +-- of load time to your config +{ "olexsmir/gopher.nvim", - requires = { -- dependencies - "nvim-lua/plenary.nvim", - "nvim-treesitter/nvim-treesitter", - }, -} -``` - -Also, run `TSInstall go` if `go` parser if isn't installed yet. - -## Config - -By `.setup` function you can configure the plugin. - -Note: - -- Installer does not install the tool in user set path - -```lua -require("gopher").setup { - commands = { - go = "go", - gomodifytags = "gomodifytags", - gotests = "~/go/bin/gotests", -- also you can set custom command path - impl = "impl", - }, + ft = "go", + -- branch = "develop" + -- (optional) updates the plugin's dependencies on each update + build = function() + vim.cmd.GoInstallDeps() + end, + ---@module "gopher" + ---@type gopher.Config + opts = {}, } ``` ## Features -1. Installation requires this go tool: +
+ + Install plugin's go deps + -```vim -:GoInstallDeps + ```vim + :GoInstallDeps + ``` + + This will install the following tools: + + - [gomodifytags](https://github.com/fatih/gomodifytags) + - [impl](https://github.com/josharian/impl) + - [gotests](https://github.com/cweill/gotests) + - [iferr](https://github.com/koron/iferr) + - [json2go](https://github.com/olexsmir/json2go) +
+ +
+ + Add and remove tags for structs via gomodifytags + + + ![Add tags demo](./vhs/tags.gif) + + By default `json` tag will be added/removed, if not set: + + ```vim + " add json tag + :GoTagAdd json + + " add json tag with omitempty option + :GoTagAdd json=omitempty + + " remove yaml tag + :GoTagRm yaml + ``` + + ```lua + -- or you can use lua api + require("gopher").tags.add "xml" + require("gopher").tags.rm "proto" + ``` +
+ +
+ + Generating tests via gotests + + + ```vim + " Generate one test for a specific function/method(one under cursor) + :GoTestAdd + + " Generate all tests for all functions/methods in the current file + :GoTestsAll + + " Generate tests for only exported functions/methods in the current file + :GoTestsExp + ``` + + ```lua + -- or you can use lua api + require("gopher").test.add() + require("gopher").test.exported() + require("gopher").test.all() + ``` + + For named tests see `:h gopher.nvim-gotests-named` +
+ +
+ + Run commands like go mod/get/etc inside of nvim + + + ```vim + :GoGet github.com/gorilla/mux + + " Link can have an `http` or `https` prefix. + :GoGet https://github.com/lib/pq + + " You can provide more than one package url + :GoGet github.com/jackc/pgx/v5 github.com/google/uuid/ + + " go mod commands + :GoMod tidy + :GoMod init new-shiny-project + + " go work commands + :GoWork sync + + " run go generate in cwd + :GoGenerate + + " run go generate for the current file + :GoGenerate % + ``` +
+ +
+ + Interface implementation via impl + + + ![Auto interface implementation demo](./vhs/impl.gif) + + Syntax of the command: + ```vim + :GoImpl [receiver] [interface] + + " also you can put a cursor on the struct and run + :GoImpl [interface] + ``` + + Usage examples: + ```vim + :GoImpl r Read io.Reader + :GoImpl Write io.Writer + + " or you can simply put a cursor on the struct and run + :GoImpl io.Reader + ``` +
+ +
+ + Generate boilerplate for doc comments + + + ![Generate comments](./vhs/comment.gif) + + First set a cursor on **public** package/function/interface/struct and execute: + + ```vim + :GoCmt + ``` +
+ +
+ + Convert json to Go types + + + ![Convert JSON to Go types](./vhs/json2go.gif) + + `:GoJson` opens a temporary buffer where you can paste or write JSON. + Saving the buffer (`:w` or `:wq`) automatically closes it and inserts the generated Go struct into the original buffer at the cursor position. + + Alternatively, you can pass JSON directly as an argument: + ```vim + :GoJson {"name": "Alice", "age": 30} + ``` + + Additionally, `gopher.json2go` provides lua api, see `:h gopher.nvim-json2go` for details. +
+ + +
+ + Generate if err != nil { via iferr + + + ![Generate if err != nil {](./vhs/iferr.gif) + + Set the cursor on the line with `err` and execute + + ```vim + :GoIfErr + ``` +
+ +## Configuration + +> [!IMPORTANT] +> +> If you need more info look `:h gopher.nvim` + +**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, + + -- timeout for running installer commands(e.g :GoDepsInstall, :GoDepsInstallSync) + installer_timeout = 999999, + + -- user specified paths to binaries + commands = { + go = "go", + gomodifytags = "gomodifytags", + gotests = "gotests", + impl = "impl", + iferr = "iferr", + }, + gotests = { + -- a default template that gotess will use. + -- gotets doesn't have template named `default`, we use it to represent absence of the provided template. + template = "default", + + -- path to a directory containing custom test code templates + template_dir = nil, + + -- use named tests(map with test name as key) in table tests(slice of structs by default) + named = false, + }, + gotag = { + transform = "snakecase", + + -- default tags to add to struct fields + default_tag = "json", + + -- default tag option added struct fields, set to nil to disable + -- e.g: `option = "json=omitempty,xml=omitempty` + option = nil, + }, + iferr = { + -- choose a custom error message, nil to use default + -- e.g: `message = 'fmt.Errorf("failed to %w", err)'` + message = nil, + }, + json2go = { + -- command used to open interactive input. + -- e.g: `split`, `botright split`, `tabnew` + interactive_cmd = "vsplit", + + -- name of autogenerated struct + -- e.g: "MySuperCoolName" + type_name = nil, + }, +} ``` -It will install next tools: +## Troubleshooting +The most common issue with the plugin is missing dependencies. +Run `:checkhealth gopher` to verify that the plugin is installed correctly. +If any binaries are missing, install them using `:GoInstallDeps`. -- [gomodifytags](https://github.com/fatih/gomodifytags) -- [impl](https://github.com/josharian/impl) -- [gotests](https://github.com/cweill/gotests) +If the issue persists, feel free to [open a new issue](https://github.com/olexsmir/gopher.nvim/issues/new). -2. Modify struct tags: -By default `json` tag will be added/removed, if not set: +## Contributing -```vim -:GoTagAdd json " For add json tag -:GoTagRm yaml " For remove yaml tag -``` - -3. Run `go mod` command: - -```vim -:GoMod tidy " Runs `go mod tidy` -:GoMod init asdf " Runs `go mod init asdf` -``` - -4. Run `go get` command - -Link can have a `http` or `https` prefix. - -You can provide more than one package url: - -```vim -:GoGet github.com/gorilla/mux -``` - -5. Interface implementation - -Command syntax: - -```vim -:GoImpl [receiver] [interface] - -" Also you can put cursor on the struct and run: -:GoImpl [interface] -``` - -Example of usage: - -```vim -" Example -:GoImpl r Read io.Reader -" or simply put your cursor in the struct and run: -:GoImpl io.Reader -``` - -6. Generate tests with [gotests](https://github.com/cweill/gotests) - -Generate one test for spesific function/method: - -```vim -:GoTestAdd -``` - -Generate all tests for all functions/methods in current file: - -```vim -:GoTestsAll -``` - -Generate tests only for exported functions/methods in current file: - -```vim -:GoTestsExp -``` - -7. Run `go generate` command; - -```vim -" Run `go generate` in cwd path -:GoGenerate - -" Run `go generate` for current file -:GoGenerate % -``` - -8. Generate doc comment - -First set a cursor on **public** package/function/interface/struct and execure: - -```vim -:GoCmt -``` - -## Thanks: - -- [go.nvim](https://github.com/ray-x/go.nvim) +PRs are always welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..2905634 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,42 @@ +version: "3" + +includes: + vhs: + taskfile: ./vhs/Taskfile.yml + dir: ./vhs + +tasks: + lint: + cmds: + - selene . + - stylua --check . + + format: + cmd: stylua . + + test: + cmds: + - | + nvim --clean --headless \ + -u ./scripts/minimal_init.lua \ + -c "lua MiniTest.run()" \ + -c ":qa!" + nvim: + cmd: nvim --clean -u "./scripts/minimal_init.lua" {{ .CLI_ARGS }} + + docgen: + desc: generate vimhelp + cmds: + - | + nvim --clean --headless \ + -u "./scripts/minimal_init.lua" \ + -c "luafile ./scripts/docgen.lua" \ + -c ":qa!" + + install-deps: + desc: installs go bin (used in CI) + cmds: + - | + nvim --clean --headless \ + -u "./scripts/minimal_init.lua" \ + +GoInstallDepsSync +qa diff --git a/autoload/health/gopher.vim b/autoload/health/gopher.vim deleted file mode 100644 index 7ff5205..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/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..61ffc7c --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +/tags diff --git a/doc/gopher.nvim.txt b/doc/gopher.nvim.txt new file mode 100644 index 0000000..9232031 --- /dev/null +++ b/doc/gopher.nvim.txt @@ -0,0 +1,294 @@ +*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 + Setup ................................................ |gopher.nvim-setup()| + Install dependencies ............................ |gopher.nvim-dependencies| + Config ................................................ |gopher.nvim-config| + Commands ............................................ |gopher.nvim-commands| + Modify struct tags ............................... |gopher.nvim-struct-tags| + json2go .............................................. |gopher.nvim-json2go| + 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.setup`({user_config}) +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. +See |gopher.nvim.config| + +Usage ~ +>lua + require("gopher").setup {} -- use default config or replace {} with your own +< +Parameters ~ +{user_config} `(gopher.Config)` See |gopher.nvim-config| + +------------------------------------------------------------------------------ + *gopher.nvim-dependencies* + `gopher.install_deps` + +Gopher.nvim implements most of its features using third-party tools. To +install plugin's dependencies, you can run: +`:GoInstallDeps` or `:GoInstallDepsSync` +or use `require("gopher").install_deps()` if you prefer lua api. + + +============================================================================== +------------------------------------------------------------------------------ + *gopher.nvim-config* + `default_config` +>lua + local default_config = { + -- 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, + + -- timeout for running installer commands(e.g :GoDepsInstall, :GoDepsInstallSync) + ---@type number + installer_timeout = 999999, + + -- user specified paths to binaries + ---@class gopher.ConfigCommand + commands = { + go = "go", + gomodifytags = "gomodifytags", + gotests = "gotests", + impl = "impl", + iferr = "iferr", + json2go = "json2go", + }, + ---@class gopher.ConfigGotests + gotests = { + -- a default template that gotess will use. + -- gotets doesn't have template named `default`, we use it to represent absence of the provided template. + template = "default", + + -- path to a directory containing custom test code templates + ---@type string|nil + template_dir = nil, + + -- use named tests(map with test name as key) in table tests(slice of structs by default) + named = false, + }, + ---@class gopher.ConfigGoTag + gotag = { + ---@type gopher.ConfigGoTagTransform + transform = "snakecase", + + -- default tags to add to struct fields + default_tag = "json", + + -- default tag option added struct fields, set to nil to disable + -- e.g: `option = "json=omitempty,xml=omitempty` + ---@type string|nil + option = nil, + }, + ---@class gopher.ConfigIfErr + iferr = { + -- choose a custom error message, nil to use default + -- e.g: `message = 'fmt.Errorf("failed to %w", err)'` + ---@type string|nil + message = nil, + }, + ---@class gopher.ConfigJson2Go + json2go = { + -- command used to open interactive input. + -- e.g: `split`, `botright split`, `tabnew` + interactive_cmd = "vsplit", + + -- name of autogenerated struct, if nil none, will the default one of json2go. + -- e.g: "MySuperCoolName" + ---@type string|nil + type_name = nil, + }, + } +< +Class ~ +{gopher.Config} +Fields ~ +{setup} `(fun(user_config?: 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. + +Usage ~ + +How to add/remove/clear 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 +4. Run `:GoTagClear` to clear all tags from struct fields + +If you want to add/remove tag with options, you can use `json=omitempty` +(where json is tag, and omitempty is its option). +Example: `:GoTagAdd xml json=omitempty` + + +NOTE: if you dont specify the tag it will use `json` as default + +Example: +>go + // before + type User struct { + // ^ put your cursor here + // run `:GoTagAdd yaml` + ID int + Name string + } + + // after + type User struct { + ID int `yaml:id` + Name string `yaml:name` + } +< + +============================================================================== +------------------------------------------------------------------------------ + *gopher.nvim-json2go* + +Convert json to go type annotations. + +Usage ~ + +`:GoJson` opens a temporary buffer where you can paste or write JSON. +Saving the buffer (`:w` or `:wq`) automatically closes it and inserts the +generated Go struct into the original buffer at the cursor position. + +Alternatively, you can pass JSON directly as an argument: +>vim + :GoJson {"name": "Alice", "age": 30} +< +------------------------------------------------------------------------------ + *json2go.transform()* + `json2go.transform`({json_str}) + +Parameters ~ +{json_str} `(string)` Json string that is going to be converted to go type. +Return ~ +`(string)` `(optional)` Go type, or nil if failed. + +------------------------------------------------------------------------------ + *json2go.json2go()* + `json2go.json2go`({json_str}) +Converts json string to go type, and puts result under the cursor. If +[json_str] is nil, will open an interactive prompt (with cmd set in +config). + +Parameters ~ +{json_str} `(optional)` `(string)` + + +============================================================================== +------------------------------------------------------------------------------ + *gopher.nvim-impl* + +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. + +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 + // run `:GoImpl b io.Reader` + + // this is what you will get + func (b *BytesReader) Read(p []byte) (n int, err error) { + panic("not implemented") // TODO: Implement + } +< + +============================================================================== +------------------------------------------------------------------------------ + *gopher.nvim-gotests* +gotests is utilizing the `gotests` tool to generate unit tests boilerplate. +Usage ~ + +- 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: + - run `:GoTestsAll` + +- 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: https://github.com/cweill/gotests + +If you prefer named tests, you can enable them in |gopher.nvim-config|. + + +============================================================================== +------------------------------------------------------------------------------ + *gopher.nvim-iferr* + +`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 + + +============================================================================== +------------------------------------------------------------------------------ + *gopher.nvim-comments* + +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. + + + vim:tw=78:ts=8:noet:ft=help:norl: \ No newline at end of file diff --git a/lua/gopher/_utils/init.lua b/lua/gopher/_utils/init.lua index 6f41e22..58a17b2 100644 --- a/lua/gopher/_utils/init.lua +++ b/lua/gopher/_utils/init.lua @@ -1,56 +1,72 @@ -return { - ---@param t table - ---@return boolean - empty = function(t) - if t == nil then - return true +local c = require "gopher.config" +local log = require "gopher._utils.log" +local utils = {} + +---@param msg string +---@param lvl? integer by default `vim.log.levels.INFO` +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 + +---@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 - return next(t) == nil - end, +---@param s string +---@return string +function utils.trimend(s) + local r, _ = string.gsub(s, "%s+$", "") + return r +end - ---@param s string - ---@return string - rtrim = function(s) - local n = #s - while n > 0 and s:find("^%s", n) do - n = n - 1 +-- Since indentation can be spaces or tabs, that's my hack around it +---@param line string +---@param indent integer +---@return string +function utils.indent(line, indent) + local char = string.sub(line, 1, 1) + if char ~= " " and char ~= "\t" then + char = " " + end + return string.rep(char, indent) +end + +---@generic T +---@param tbl T[] +---@return T[] +function utils.list_unique(tbl) + if vim.fn.has "nvim-0.12" == 1 then + return vim.list.unique(tbl) + end + + for i = #tbl, 1, -1 do + for j = 1, i - 1 do + if tbl[i] == tbl[j] then + table.remove(tbl, i) + break + end end + end + return tbl +end - return s:sub(1, n) - end, - - ---@param lib string - ---@return boolean - lualib_is_found = function(lib) - local is_found, _ = pcall(require, lib) - return is_found - end, - - ---@param bin string - ---@return boolean - binary_is_found = function(bin) - if vim.fn.executable(bin) == 1 then - return true - end - - return false - end, - - ---@param msg string - ---@param lvl string|integer - notify = function(msg, lvl) - local l - if lvl == "error" or lvl == 4 then - l = vim.log.levels.ERROR - elseif lvl == "info" or lvl == 2 then - l = vim.log.levels.INFO - elseif lvl == "debug" or lvl == 1 then - l = vim.log.levels.DEBUG - end - - vim.defer_fn(function() - vim.notify(msg, l) - end, 0) - end, -} +return utils diff --git a/lua/gopher/_utils/log.lua b/lua/gopher/_utils/log.lua new file mode 100644 index 0000000..105f53a --- /dev/null +++ b/lua/gopher/_utils/log.lua @@ -0,0 +1,171 @@ +-- thanks https://github.com/tjdevries/vlog.nvim +-- and https://github.com/williamboman/mason.nvim +-- for the code i have stolen(or have inspected by idk) +local c = require "gopher.config" + +---@class gopher.Logger +---@field get_outfile fun():string +---@field trace fun(...) +---@field fmt_trace fun(...) +---@field debug fun(...) +---@field fmt_debug fun(...) +---@field info fun(...) +---@field fmt_info fun(...) +---@field warn fun(...) +---@field fmt_warn fun(...) +---@field error fun(...) +---@field fmt_error fun(...) + +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 + -- values: 'sync','async',false + use_console = vim.env.GOPHER_VERBOSE_LOGS == "1", + + -- Should highlighting be used in console (using echohl) + highlights = true, + + -- Should write to a file + use_file = true, + + -- Level configuration + modes = { + { name = "trace", hl = "Comment", level = vim.log.levels.TRACE }, + { name = "debug", hl = "Comment", level = vim.log.levels.DEBUG }, + { name = "info", hl = "None", level = vim.log.levels.INFO }, + { name = "warn", hl = "WarningMsg", level = vim.log.levels.WARN }, + { name = "error", hl = "ErrorMsg", level = vim.log.levels.ERROR }, + }, + + -- Can limit the number of decimals displayed for floats + float_precision = 0.01, +} + +---@type gopher.Logger +---@diagnostic disable-next-line: missing-fields +local log = {} + +---@return string +function log.get_outfile() + return table.concat { + (vim.fn.has "nvim-0.8.0" == 1) and vim.fn.stdpath "log" or vim.fn.stdpath "cache", + ("/%s.log"):format(config.name), + } +end + +-- selene: allow(incorrect_standard_library_use) +local unpack = unpack or table.unpack + +do + local round = function(x, increment) + increment = increment or 1 + x = x / increment + return (x > 0 and math.floor(x + 0.5) or math.ceil(x - 0.5)) * increment + end + + local tbl_has_tostring = function(tbl) + local mt = getmetatable(tbl) + return mt and mt.__tostring ~= nil + end + + local make_string = function(...) + local t = {} + for i = 1, select("#", ...) do + local x = select(i, ...) + + if type(x) == "number" and config.float_precision then + x = tostring(round(x, config.float_precision)) + elseif type(x) == "table" and not tbl_has_tostring(x) then + x = vim.inspect(x) + else + x = tostring(x) + end + + t[#t + 1] = x + end + return table.concat(t, " ") + end + + 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 doesn't work + if level_config.level < c.log_level then + return + end + local nameupper = level_config.name:upper() + + local msg = message_maker(...) + local info = debug.getinfo(2, "Sl") + local lineinfo = info.short_src .. ":" .. info.currentline + + -- Output to console + if config.use_console then + local log_to_console = function() + local console_string = + string.format("[%-6s%s] %s: %s", nameupper, os.date "%H:%M:%S", lineinfo, msg) + + if config.highlights and level_config.hl then + vim.cmd(string.format("echohl %s", level_config.hl)) + end + + local split_console = vim.split(console_string, "\n") + for _, v in ipairs(split_console) do + local formatted_msg = string.format("[%s] %s", config.name, vim.fn.escape(v, [["\]])) + + ---@diagnostic disable-next-line: param-type-mismatch + local ok = pcall(vim.cmd, string.format([[echom "%s"]], formatted_msg)) + if not ok then + vim.api.nvim_out_write(msg .. "\n") + end + end + + if config.highlights and level_config.hl then + vim.cmd "echohl NONE" + end + end + if config.use_console == "sync" and not vim.in_fast_event() then + log_to_console() + else + vim.schedule(log_to_console) + end + end + + -- Output to log file + if config.use_file then + local fp = assert(io.open(log.get_outfile(), "a")) + local str = string.format("[%-6s%s] %s: %s\n", nameupper, os.date(), lineinfo, msg) + fp:write(str) + fp:close() + end + end + + for _, x in ipairs(config.modes) do + -- log.info("these", "are", "separated") + log[x.name] = function(...) ---@diagnostic disable-line: assign-type-mismatch + return log_at_level(x, make_string, ...) + end + + -- log.fmt_info("These are %s strings", "formatted") + log[("fmt_%s"):format(x.name)] = function(...) ---@diagnostic disable-line: assign-type-mismatch + return log_at_level(x, function(...) + local passed = { ... } + local fmt = table.remove(passed, 1) + local inspected = {} + for _, v in ipairs(passed) do + if type(v) == "table" and tbl_has_tostring(v) then + table.insert(inspected, v) + else + table.insert(inspected, vim.inspect(v)) + end + end + return string.format(fmt, unpack(inspected)) + end, ...) + end + end +end + +return log 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/ts.lua b/lua/gopher/_utils/ts.lua new file mode 100644 index 0000000..d24fb09 --- /dev/null +++ b/lua/gopher/_utils/ts.lua @@ -0,0 +1,160 @@ +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))))] + ]], + struct_field = [[ + (field_declaration name: (field_identifier) @_name) + ]], + func = [[ + [(function_declaration name: (identifier) @_name) + (method_declaration name: (field_identifier) @_name) + (method_elem name: (field_identifier) @_name)] + ]], + package = [[ + (package_identifier) @_name + ]], + interface = [[ + (type_spec + name: (type_identifier) @_name + type: (interface_type)) + ]], + var = [[ + [(var_declaration (var_spec name: (identifier) @_name)) + (short_var_declaration + left: (expression_list (identifier) @_name @_var))] + ]], +} + +---@param parent_type string[] +---@param node TSNode +---@return TSNode? +local function get_parent_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 Name of the struct, function, etc +---@field start integer Line number where the declaration starts +---@field end_ integer Line number where the declaration ends +---@field indent integer Number of spaces/tabs in the current cursor line +---@field is_varstruct boolean Is struct declared as `var S struct{}` or `s := struct{}{}` + +---@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 the cursor" + end + + local parent_node = get_parent_node(parent_type, node) + if not parent_node then + error "No parent node found under the 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, start_col, end_row, _ = parent_node:range() + res["indent"] = start_col + res["start"] = start_row + 1 + res["end_"] = 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 struct{} )` + --- + --- var_declaration is for cases like `var x struct{}` + --- short_var_declaration is for cases like `x := struct{}{}` + --- + --- it always chooses last struct type in the list + return do_stuff(bufnr, { + "type_spec", + "type_declaration", + "var_declaration", + "short_var_declaration", + }, queries.struct) +end + +---@param bufnr integer +function ts.get_struct_field_under_cursor(bufnr) + return do_stuff(bufnr, { "field_declaration" }, queries.struct_field) +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, { + "method_elem", + "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 + +---@param bufnr integer +function ts.get_variable_under_cursor(bufnr) + return do_stuff(bufnr, { "var_declaration", "short_var_declaration" }, queries.var) +end + +return ts diff --git a/lua/gopher/_utils/ts/init.lua b/lua/gopher/_utils/ts/init.lua deleted file mode 100644 index 6bcf8fc..0000000 --- a/lua/gopher/_utils/ts/init.lua +++ /dev/null @@ -1,87 +0,0 @@ -local nodes = require "gopher._utils.ts.nodes" -local u = require "gopher._utils" -local M = { - 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 ----@return table|nil -function M.get_struct_node_at_pos(row, col, bufnr) - local query = M.querys.struct_block .. " " .. M.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 - u.notify("struct not found", "warn") - else - return ns[#ns] - end -end - ----@param row string ----@param col string ----@param bufnr string|nil ----@return table|nil -function M.get_func_method_node_at_pos(row, col, bufnr) - local query = M.querys.func .. " " .. M.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 - u.notify("function not found", "warn") - else - return ns[#ns] - end -end - ----@param row string ----@param col string ----@param bufnr string|nil ----@return table|nil -function M.get_package_node_at_pos(row, col, bufnr) - -- stylua: ignore - if row > 10 then return end - local query = M.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 - u.notify("package not found", "warn") - return nil - else - return ns[#ns] - end -end - ----@param row string ----@param col string ----@param bufnr string|nil ----@return table|nil -function M.get_interface_node_at_pos(row, col, bufnr) - local query = M.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 - u.notify("interface not found", "warn") - else - return ns[#ns] - end -end - -return M diff --git a/lua/gopher/_utils/ts/nodes.lua b/lua/gopher/_utils/ts/nodes.lua deleted file mode 100644 index 9b5b813..0000000 --- a/lua/gopher/_utils/ts/nodes.lua +++ /dev/null @@ -1,137 +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.parse_query(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.query.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.notify("Unable to find any nodes. Place your cursor on a go symbol and try again", "debug") - return nil - end - - nodes = M.sort_nodes(M.intersect_nodes(nodes, row, col)) - if nodes == nil or #nodes == 0 then - u.notify("Unable to find any nodes at pos. " .. tostring(row) .. ":" .. tostring(col), "debug") - return nil - end - - return nodes -end - -return M diff --git a/lua/gopher/comment.lua b/lua/gopher/comment.lua index 3991651..4ce264c 100644 --- a/lua/gopher/comment.lua +++ b/lua/gopher/comment.lua @@ -1,50 +1,71 @@ -local ts_utils = require "gopher._utils.ts" +---@toc_entry Generate comments +---@tag gopher.nvim-comments +---@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 function generate(row, col) - local comment, ns = nil, nil +local ts = require "gopher._utils.ts" +local log = require "gopher._utils.log" +local u = require "gopher._utils" +local comment = {} - ns = ts_utils.get_package_node_at_pos(row, col) - if ns ~= nil and ns ~= {} then - comment = "// Package " .. ns.name .. " provides " .. ns.name - return comment, ns +--- NOTE: The order of functions executed inside this function is IMPORTANT. +--- This function is extremely fragile; run tests after making any changes. +--- +---@param bufnr integer +---@param line string +---@return string +---@dochide +local function generate(bufnr, line) + local sf_ok, sf_res = pcall(ts.get_struct_field_under_cursor, bufnr) + if sf_ok then + return u.indent(line, sf_res.indent) .. "// " .. sf_res.name .. " " end - ns = ts_utils.get_struct_node_at_pos(row, col) - if ns ~= nil and ns ~= {} then - comment = "// " .. ns.name .. " " .. ns.type .. " " - return comment, ns + local s_ok, s_res = pcall(ts.get_struct_under_cursor, bufnr) + if s_ok then + return u.indent(line, s_res.indent) .. "// " .. s_res.name .. " " end - ns = ts_utils.get_func_method_node_at_pos(row, col) - if ns ~= nil and ns ~= {} then - comment = "// " .. ns.name .. " " .. ns.type .. " " - return comment, ns + local v_ok, v_res = pcall(ts.get_variable_under_cursor, bufnr) + if v_ok then + return u.indent(line, v_res.indent) .. "// " .. v_res.name .. " " end - ns = ts_utils.get_interface_node_at_pos(row, col) - if ns ~= nil and ns ~= {} then - comment = "// " .. ns.name .. " " .. ns.type .. " " - return comment, ns + local f_ok, f_res = pcall(ts.get_func_under_cursor, bufnr) + if f_ok then + return u.indent(line, f_res.indent) .. "// " .. f_res.name .. " " end - return "// ", {} + local i_ok, i_res = pcall(ts.get_interface_under_cursor, bufnr) + if i_ok then + return u.indent(line, i_res.indent) .. "// " .. i_res.name .. " " + end + + local p_ok, p_res = pcall(ts.get_package_under_cursor, bufnr) + if p_ok then + return "// Package " .. p_res.name .. " provides " + end + + return "// " end -return function() - local row, col = unpack(vim.api.nvim_win_get_cursor(0)) - local comment, ns = generate(row + 1, col + 1) - - vim.api.nvim_win_set_cursor(0, { - ns.dim.s.r, - ns.dim.s.c, +function comment.comment() + local bufnr = vim.api.nvim_get_current_buf() + local lnum = vim.fn.getcurpos()[2] + local line = vim.fn.getline(lnum) + local cmt = generate(bufnr, line) + log.debug("generated comment:", { + comment = cmt, + line = line, }) - vim.fn.append(row - 1, comment) - - vim.api.nvim_win_set_cursor(0, { - ns.dim.s.r, - #comment + 1, - }) - - vim.cmd [[startinsert!]] + vim.fn.append(lnum - 1, cmt) + vim.fn.setpos(".", { bufnr, lnum, #cmt }) + vim.cmd "startinsert!" end + +return comment diff --git a/lua/gopher/config.lua b/lua/gopher/config.lua index 8f77dc7..4245989 100644 --- a/lua/gopher/config.lua +++ b/lua/gopher/config.lua @@ -1,19 +1,142 @@ -local M = { - config = { - ---set custom commands for tools - commands = { - go = "go", - gomodifytags = "gomodifytags", - gotests = "gotests", - impl = "impl", - }, +---@type gopher.Config +---@dochide +---@diagnostic disable-next-line: missing-fields .setup() gets injected later +local config = {} + +---@tag gopher.nvim-config.ConfigGoTagTransform +---@text Possible values for |gopher.Config|.gotag.transform: +--- +---@dochide +---@alias gopher.ConfigGoTagTransform +---| "snakecase" "GopherUser" -> "gopher_user" +---| "camelcase" "GopherUser" -> "gopherUser" +---| "lispcase" "GopherUser" -> "gopher-user" +---| "pascalcase" "GopherUser" -> "GopherUser" +---| "titlecase" "GopherUser" -> "Gopher User" +---| "keep" keeps the original field name + +---@toc_entry Config +---@tag gopher.nvim-config +---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section) +---@class gopher.Config +---@field setup fun(user_config?: gopher.Config) +local default_config = { + -- 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, + + -- timeout for running installer commands(e.g :GoDepsInstall, :GoDepsInstallSync) + ---@type number + installer_timeout = 999999, + + -- user specified paths to binaries + ---@class gopher.ConfigCommand + commands = { + go = "go", + gomodifytags = "gomodifytags", + gotests = "gotests", + impl = "impl", + iferr = "iferr", + json2go = "json2go", + }, + ---@class gopher.ConfigGotests + gotests = { + -- a default template that gotess will use. + -- gotets doesn't have template named `default`, we use it to represent absence of the provided template. + template = "default", + + -- path to a directory containing custom test code templates + ---@type string|nil + template_dir = nil, + + -- use named tests(map with test name as key) in table tests(slice of structs by default) + named = false, + }, + ---@class gopher.ConfigGoTag + gotag = { + ---@type gopher.ConfigGoTagTransform + transform = "snakecase", + + -- default tags to add to struct fields + default_tag = "json", + + -- default tag option added struct fields, set to nil to disable + -- e.g: `option = "json=omitempty,xml=omitempty` + ---@type string|nil + option = nil, + }, + ---@class gopher.ConfigIfErr + iferr = { + -- choose a custom error message, nil to use default + -- e.g: `message = 'fmt.Errorf("failed to %w", err)'` + ---@type string|nil + message = nil, + }, + ---@class gopher.ConfigJson2Go + json2go = { + -- command used to open interactive input. + -- e.g: `split`, `botright split`, `tabnew` + interactive_cmd = "vsplit", + + -- name of autogenerated struct, if nil none, will the default one of json2go. + -- e.g: "MySuperCoolName" + ---@type string|nil + type_name = nil, }, } +--minidoc_afterlines_end ----Plugin setup function ----@param opts table user options -function M.setup(opts) - M.config = vim.tbl_deep_extend("force", M.config, opts) +---@type gopher.Config +---@dochide +local _config = default_config + +-- I am kinda secret so don't tell anyone about me even dont use me +-- +-- 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 +---@dochide +function config.setup(user_config) + 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") + vim.validate("timeout", _config.timeout, "number") + vim.validate("installer_timeout", _config.timeout, "number") + vim.validate("commands", _config.commands, "table") + vim.validate("commands.go", _config.commands.go, "string") + vim.validate("commands.gomodifytags", _config.commands.gomodifytags, "string") + vim.validate("commands.gotests", _config.commands.gotests, "string") + vim.validate("commands.impl", _config.commands.impl, "string") + vim.validate("commands.iferr", _config.commands.iferr, "string") + vim.validate("commands.json2go", _config.commands.json2go, "string") + vim.validate("gotests", _config.gotests, "table") + vim.validate("gotests.template", _config.gotests.template, "string") + vim.validate("gotests.template_dir", _config.gotests.template_dir, { "string", "nil" }) + vim.validate("gotests.named", _config.gotests.named, "boolean") + vim.validate("gotag", _config.gotag, "table") + vim.validate("gotag.transform", _config.gotag.transform, "string") + vim.validate("gotag.default_tag", _config.gotag.default_tag, "string") + vim.validate("gotag.option", _config.gotag.option, { "string", "nil" }) + vim.validate("iferr", _config.iferr, "table") + vim.validate("iferr.message", _config.iferr.message, { "string", "nil" }) + vim.validate("json2go.installer_timeout", _config.json2go.interactive_cmd, "string") + vim.validate("json2go.type_name", _config.json2go.type_name, { "string", "nil" }) end -return M +setmetatable(config, { + __index = function(_, key) + return _config[key] + end, +}) + +---@dochide +return config diff --git a/lua/gopher/go.lua b/lua/gopher/go.lua new file mode 100644 index 0000000..ca57068 --- /dev/null +++ b/lua/gopher/go.lua @@ -0,0 +1,56 @@ +local c = require "gopher.config" +local u = require "gopher._utils" +local r = require "gopher._utils.runner" +local go = {} + +local function run(subcmd, args) + local rs = r.sync { c.commands.go, subcmd, unpack(args) } + if rs.code ~= 0 then + error("go " .. subcmd .. " failed: " .. rs.stderr) + end + + u.notify(c.commands.go .. " " .. subcmd .. " ran successful") + return rs.stdout +end + +---@param args string[] +function go.get(args) + for i, arg in ipairs(args) do + local m = string.match(arg, "^https://(.*)$") or string.match(arg, "^http://(.*)$") or arg + table.remove(args, i) + table.insert(args, i, m) + end + + run("get", args) +end + +---@param args string[] +function go.mod(args) + run("mod", args) +end + +---@param args string[] +function go.work(args) + -- TODO: use `gopls.tidy` + + run("work", args) +end + +---Executes `go generate` +---If only argument is `%` it's going to be equivalent to `go generate ` +---@param args string[] +function go.generate(args) + -- TODO: use `gopls.generate` + + if #args == 0 then + error "please provide arguments" + end + + if #args == 1 and args[1] == "%" then + args[1] = vim.fn.expand "%" + end + + run("generate", args) +end + +return go diff --git a/lua/gopher/gogenerate.lua b/lua/gopher/gogenerate.lua deleted file mode 100644 index 93f78da..0000000 --- a/lua/gopher/gogenerate.lua +++ /dev/null @@ -1,28 +0,0 @@ -local Job = require "plenary.job" -local c = require("gopher.config").config.commands -local u = require "gopher._utils" - ----run "go generate" -return function(...) - local args = { ... } - if #args == 1 and args[1] == "%" then - args[1] = vim.fn.expand "%" ---@diagnostic disable-line: missing-parameter - end - - local cmd_args = vim.list_extend({ "generate" }, args) - - Job - :new({ - command = c.go, - args = cmd_args, - on_exit = function(_, retval) - if retval ~= 0 then - u.notify("command 'go " .. unpack(cmd_args) .. "' exited with code " .. retval, "error") - return - end - - u.notify("go generate was success runned", "info") - end, - }) - :start() -end diff --git a/lua/gopher/goget.lua b/lua/gopher/goget.lua deleted file mode 100644 index 8d79626..0000000 --- a/lua/gopher/goget.lua +++ /dev/null @@ -1,35 +0,0 @@ -local Job = require "plenary.job" -local c = require("gopher.config").config.commands -local u = require "gopher._utils" - ----run "go get" -return function(...) - local args = { ... } - if #args == 0 then - u.notify("please provide a package url to get", "error") - return - end - - for i, arg in ipairs(args) do - local m = string.match(arg, "^https://(.*)$") or string.match(arg, "^http://(.*)$") or arg - table.remove(args, i) - table.insert(args, i, m) - end - - local cmd_args = vim.list_extend({ "get" }, args) - - Job - :new({ - command = c.go, - args = cmd_args, - on_exit = function(_, retval) - if retval ~= 0 then - u.notify("command 'go " .. unpack(cmd_args) .. "' exited with code " .. retval, "error") - return - end - - u.notify("go get was success runned", "info") - end, - }) - :start() -end diff --git a/lua/gopher/gomod.lua b/lua/gopher/gomod.lua deleted file mode 100644 index 61f19a2..0000000 --- a/lua/gopher/gomod.lua +++ /dev/null @@ -1,29 +0,0 @@ -local Job = require "plenary.job" -local c = require("gopher.config").config.commands -local u = require "gopher._utils" - ----run "go mod" -return function(...) - local args = { ... } - if #args == 0 then - u.notify("please provide any mod command", "error") - return - end - - local cmd_args = vim.list_extend({ "mod" }, args) - - Job - :new({ - command = c.go, - args = cmd_args, - on_exit = function(_, retval) - if retval ~= 0 then - u.notify("command 'go " .. unpack(cmd_args) .. "' exited with code " .. retval, "error") - return - end - - u.notify("go mod was success runned", "info") - end, - }) - :start() -end diff --git a/lua/gopher/gotests.lua b/lua/gopher/gotests.lua index c71243e..0a9eaf4 100644 --- a/lua/gopher/gotests.lua +++ b/lua/gopher/gotests.lua @@ -1,73 +1,76 @@ -local Job = require "plenary.job" +---@toc_entry Generating unit tests boilerplate +---@tag gopher.nvim-gotests +---@text gotests is utilizing the `gotests` tool to generate unit tests boilerplate. +---@usage +--- - 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: +--- - run `:GoTestsAll` +--- +--- - 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: https://github.com/cweill/gotests +--- +--- 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" -local c = require("gopher.config").config.commands +local r = require "gopher._utils.runner" local u = require "gopher._utils" -local M = {} - ----@param cmd_args table -local function run(cmd_args) - Job - :new({ - command = c.gotests, - args = cmd_args, - on_exit = function(_, retval) - if retval ~= 0 then - u.notify("command 'go " .. unpack(cmd_args) .. "' exited with code " .. retval, "error") - return - end - - u.notify("unit test(s) generated", "info") - end, - }) - :start() -end +local log = require "gopher._utils.log" +local gotests = {} ---@param args table +---@dochide local function add_test(args) - local fpath = vim.fn.expand "%" ---@diagnostic disable-line: missing-parameter + if c.gotests.named then + table.insert(args, "-named") + end + + if c.gotests.template_dir then + table.insert(args, "-template_dir") + table.insert(args, c.gotests.template_dir) + end + + if c.gotests.template ~= "default" then + table.insert(args, "-template") + table.insert(args, c.gotests.template) + end + table.insert(args, "-w") - table.insert(args, fpath) - run(args) -end + table.insert(args, vim.fn.expand "%") ----generate unit test for one function ----@param parallel boolean -function M.func_test(parallel) - 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", "info") - return + log.debug("generating tests with args: ", args) + + local rs = r.sync { c.commands.gotests, unpack(args) } + if rs.code ~= 0 then + error("gotests failed: " .. rs.stderr) end - local cmd_args = { "-only", ns.name } - if parallel then - table.insert(cmd_args, "-parallel") - end - - add_test(cmd_args) + u.notify "unit test(s) generated" end ----generate unit tests for all functions in current file ----@param parallel boolean -function M.all_tests(parallel) - local cmd_args = { "-all" } - if parallel then - table.insert(cmd_args, "-parallel") - end +-- generate unit test for one function +function gotests.func_test() + local bufnr = vim.api.nvim_get_current_buf() + local func = ts_utils.get_func_under_cursor(bufnr) - add_test(cmd_args) + add_test { "-only", func.name } end ----generate unit tests for all exported functions ----@param parallel boolean -function M.all_exported_tests(parallel) - local cmd_args = {} - if parallel then - table.insert(cmd_args, "-parallel") - end - - table.insert(cmd_args, "-exported") - add_test(cmd_args) +-- generate unit tests for all functions in current file +function gotests.all_tests() + add_test { "-all" } end -return M +-- generate unit tests for all exported functions +function gotests.all_exported_tests() + add_test { "-exported" } +end + +return gotests diff --git a/lua/gopher/health.lua b/lua/gopher/health.lua index 8a20fce..652e93c 100644 --- a/lua/gopher/health.lua +++ b/lua/gopher/health.lua @@ -1,40 +1,59 @@ -local health = vim.health or require "health" -local utils = require "gopher._utils" -local c = require("gopher.config").config.commands +local c = require("gopher.config").commands +local health = {} -local M = { - _required = { - plugins = { - { lib = "plenary" }, - { lib = "nvim-treesitter" }, - }, - binarys = { - { bin = c.go, help = "required for GoMod, GoGet, GoGenerate command" }, - { bin = c.gomodifytags, help = "required for modify struct tags" }, - { bin = c.impl, help = "required for interface implementing" }, - { bin = c.gotests, help = "required for test(s) generation" }, +local deps = { + vim_version = "nvim-0.10", + bin = { + { + bin = c.go, + msg = "required for `:GoGet`, `:GoMod`, `:GoGenerate`, `:GoWork`, `:GoInstallDeps`, `:GoInstallDepsSync`", }, + { bin = c.gomodifytags, msg = "required for `:GoTagAdd`, `:GoTagRm`" }, + { bin = c.impl, msg = "required for `:GoImpl`" }, + { bin = c.iferr, msg = "required for `:GoIfErr`" }, + { bin = c.gotests, msg = "required for `:GoTestAdd`, `:GoTestsAll`, `:GoTestsExp`" }, + }, + treesitter = { + { parser = "go", msg = "required for most of the parts of `gopher.nvim`" }, }, } -function M.check() - health.report_start "Required plugins" - for _, plugin in ipairs(M._required.plugins) do - if utils.lualib_is_found(plugin.lib) then - health.report_ok(plugin.lib .. " installed.") - else - health.report_error(plugin.lib .. " not found. Gopher.nvim will not work without it!") - end - end - - health.report_start "Required go tools" - for _, binary in ipairs(M._required.binarys) do - if utils.binary_is_found(binary.bin) then - health.report_ok(binary.bin .. " installed") - else - health.report_warn(binary.bin .. " is not installed but " .. binary.help) - end +---@param bin {bin:string, msg:string, optional:boolean} +local function check_binary(bin) + if vim.fn.executable(bin.bin) == 1 then + vim.health.ok(bin.bin .. " is found oh PATH: `" .. vim.fn.exepath(bin.bin) .. "`") + else + vim.health.error(bin.bin .. " not found on PATH, " .. bin.msg) end end -return M +---@param ts {parser:string, msg:string} +local function check_treesitter(ts) + local ok, parser = pcall(vim.treesitter.get_parser, 0, ts.parser) + if ok and parser ~= nil then + vim.health.ok("`" .. ts.parser .. "` parser is installed") + else + vim.health.error("`" .. ts.parser .. "` parser not found") + end +end + +function health.check() + vim.health.start "Neovim version" + if vim.fn.has(deps.vim_version) == 1 then + vim.health.ok "Neovim version is compatible" + else + vim.health.error(deps.vim_version .. " or newer is required") + end + + vim.health.start "Required binaries (those can be installed with `:GoInstallDeps`)" + for _, bin in ipairs(deps.bin) do + check_binary(bin) + end + + vim.health.start "Treesitter" + for _, parser in ipairs(deps.treesitter) do + check_treesitter(parser) + end +end + +return health diff --git a/lua/gopher/iferr.lua b/lua/gopher/iferr.lua new file mode 100644 index 0000000..3da4132 --- /dev/null +++ b/lua/gopher/iferr.lua @@ -0,0 +1,48 @@ +-- Thanks https://github.com/koron/iferr for vim implementation + +---@toc_entry Iferr +---@tag gopher.nvim-iferr +---@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 = {} + +function iferr.iferr() + local curb = vim.fn.wordcount().cursor_bytes + local pos = vim.fn.getcurpos()[2] + local fpath = vim.fn.expand "%" + + 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("ferr: failed. output: " .. rs.stderr) + error("iferr failed: " .. rs.stderr) + end + + vim.fn.append(pos, u.remove_empty_lines(vim.split(rs.stdout, "\n"))) + vim.cmd [[silent normal! j=2j]] + vim.fn.setpos(".", pos --[[@as integer[] ]]) +end + +return iferr diff --git a/lua/gopher/impl.lua b/lua/gopher/impl.lua index 1c504ee..338748b 100644 --- a/lua/gopher/impl.lua +++ b/lua/gopher/impl.lua @@ -1,75 +1,80 @@ -local Job = require "plenary.job" +---@toc_entry Auto implementation of interface methods +---@tag gopher.nvim-impl +---@text +--- 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. +--- +--- 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 +--- // run `:GoImpl b io.Reader` +--- +--- // this is what you will get +--- func (b *BytesReader) Read(p []byte) (n int, err error) { +--- panic("not implemented") // TODO: Implement +--- } +--- < + +local c = require("gopher.config").commands +local r = require "gopher._utils.runner" local ts_utils = require "gopher._utils.ts" -local c = require("gopher.config").config.commands local u = require "gopher._utils" +local impl = {} ----@return string -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.notify("put cursor on a struct or specify a receiver", "info") - return "" - end - - vim.api.nvim_win_set_cursor(0, { - ns.dim.e.r, - ns.dim.e.c, - }) - - return ns.name -end - -return function(...) +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 "redeaw!" - if iface == "" then - u.notify("usage: GoImpl f *File io.Reader", "info") - return - end + u.notify("arguments not provided. usage: :GoImpl f *File io.Reader", vim.log.levels.ERROR) + return elseif #args == 1 then -- :GoImpl io.Reader - recv = string.lower(recv) .. " *" .. recv - vim.cmd "redraw!" - iface = select(1, ...) + 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 - -- stylua: ignore - local cmd_args = { - "-dir", vim.fn.fnameescape(vim.fn.expand "%:p:h"), ---@diagnostic disable-line: missing-parameter - recv, - iface - } + assert(iface ~= "", "interface not provided") + assert(recv ~= "", "receiver not provided") - local res_data - Job - :new({ - command = c.impl, - args = cmd_args, - on_exit = function(data, retval) - if retval ~= 0 then - u.notify("command 'impl " .. unpack(cmd_args) .. "' exited with code " .. retval, "error") - return - end - - res_data = data:result() - end, - }) - :sync() + local dir = vim.fn.fnameescape(vim.fn.expand "%:p:h") + local rs = r.sync { c.impl, "-dir", dir, recv, iface } + if rs.code ~= 0 then + error("failed to implement interface: " .. rs.stderr) + end local pos = vim.fn.getcurpos()[2] - table.insert(res_data, 1, "") - vim.fn.append(pos, res_data) + local output = u.remove_empty_lines(vim.split(rs.stdout, "\n")) + + table.insert(output, 1, "") + vim.fn.append(pos, output) end + +return impl diff --git a/lua/gopher/init.lua b/lua/gopher/init.lua index 54ae79e..38a16da 100644 --- a/lua/gopher/init.lua +++ b/lua/gopher/init.lua @@ -1,18 +1,66 @@ +--- *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 +---@toc + +local log = require "gopher._utils.log" local tags = require "gopher.struct_tags" -local gotests = require "gopher.gotests" +local tests = require "gopher.gotests" +local go = require "gopher.go" local gopher = {} -gopher.install_deps = require "gopher.installer" -gopher.tags_add = tags.add -gopher.tags_rm = tags.remove -gopher.mod = require "gopher.gomod" -gopher.get = require "gopher.goget" -gopher.impl = require "gopher.impl" -gopher.generate = require "gopher.gogenerate" -gopher.comment = require "gopher.comment" -gopher.test_add = gotests.func_test -gopher.test_exported = gotests.all_exported_tests -gopher.tests_all = gotests.all_tests -gopher.setup = require("gopher.config").setup +---@toc_entry Setup +---@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. +--- See |gopher.nvim.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) + log.debug(vim.inspect(user_config)) +end + +---@toc_entry Install dependencies +---@tag gopher.nvim-dependencies +---@text +--- Gopher.nvim implements most of its features using third-party tools. To +--- install plugin's dependencies, you can run: +--- `:GoInstallDeps` or `:GoInstallDepsSync` +--- or use `require("gopher").install_deps()` if you prefer lua api. +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").comment + +gopher.tags = { + add = tags.add, + rm = tags.remove, + clear = tags.clear, +} + +gopher.test = { + add = tests.func_test, + exported = tests.all_exported_tests, + all = tests.all_tests, +} + +gopher.get = go.get +gopher.mod = go.mod +gopher.work = go.work +gopher.generate = go.generate return gopher diff --git a/lua/gopher/installer.lua b/lua/gopher/installer.lua index cfd7a00..61e1788 100644 --- a/lua/gopher/installer.lua +++ b/lua/gopher/installer.lua @@ -1,34 +1,66 @@ -local Job = require "plenary.job" +local c = require "gopher.config" +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/...", + gomodifytags = "github.com/fatih/gomodifytags@latest", + impl = "github.com/josharian/impl@latest", + gotests = "github.com/cweill/gotests/...@develop", + iferr = "github.com/koron/iferr@latest", + json2go = "olexsmir.xyz/json2go/cmd/json2go@latest", } ----@param pkg string -local function install(pkg) - local url = urls[pkg] .. "@latest" +---@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) - Job - :new({ - command = "go", - args = { "install", url }, - on_exit = function(_, retval) - if retval ~= 0 then - u.notify("command 'go install " .. url .. "' exited with code " .. retval, "error") - return - end + log.error("go install failed:", "url", url, "opt", vim.inspect(opt)) + return + end - u.notify("install " .. url .. " finished", "info ") - end, - }) - :start() + vim.schedule(function() + u.notify("go install-ed: " .. url) + end) +end + +---@param url string +local function install(url) + vim.schedule(function() + u.notify("go install-ing: " .. url) + end) + + r.async({ c.commands.go, "install", url }, function(opt) + handle_intall_exit(opt, url) + end, { timeout = c.installer_timeout }) +end + +---@param url string +local function install_sync(url) + vim.schedule(function() + u.notify("go install-ing: " .. url) + end) + + local rs = r.sync({ c.commands.go, "install", url }, { timeout = c.installer_timeout }) + handle_intall_exit(rs, url) end ---Install required go deps -return function() - 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 + +return installer diff --git a/lua/gopher/json2go.lua b/lua/gopher/json2go.lua new file mode 100644 index 0000000..40d0e9f --- /dev/null +++ b/lua/gopher/json2go.lua @@ -0,0 +1,137 @@ +---@toc_entry json2go +---@tag gopher.nvim-json2go +---@text +--- Convert json to go type annotations. +--- +---@usage +--- `:GoJson` opens a temporary buffer where you can paste or write JSON. +--- Saving the buffer (`:w` or `:wq`) automatically closes it and inserts the +--- generated Go struct into the original buffer at the cursor position. +--- +--- Alternatively, you can pass JSON directly as an argument: +--- >vim +--- :GoJson {"name": "Alice", "age": 30} +--- < + +local c = require "gopher.config" +local log = require "gopher._utils.log" +local u = require "gopher._utils" +local r = require "gopher._utils.runner" +local json2go = {} + +---@dochide +---@param bufnr integer +---@param cpos integer +---@param type_ string +local function apply(bufnr, cpos, type_) + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local input_lines = u.remove_empty_lines(vim.split(type_, "\n")) + for i, line in pairs(input_lines) do + table.insert(lines, cpos + i, line) + end + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) +end + +-- Convert json string to go type. +--- +---@param json_str string Json string that is going to be converted to go type. +---@return string? Go type, or nil if failed. +function json2go.transform(json_str) + local cmd = { c.commands.json2go } + if c.json2go.type_name then + table.insert(cmd, "-type", c.json2go.type_name) + end + + local rs = r.sync(cmd, { stdin = json_str }) + if rs.code ~= 0 then + u.notify("json2go: got this error: " .. rs.stdout, vim.log.levels.ERROR) + log.error("json2go: got this error: " .. rs.stdout) + return + end + return rs.stdout +end + +---@dochide +---@param ocpos integer +local function interactive(ocpos) + local obuf = vim.api.nvim_get_current_buf() + local owin = vim.api.nvim_get_current_win() + + -- setup the input window + local buf = vim.api.nvim_create_buf(false, true) + vim.cmd(c.json2go.interactive_cmd) + + local win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(win, buf) + vim.api.nvim_buf_set_name(buf, "[GoJson input]") + vim.api.nvim_set_option_value("filetype", "jsonc", { buf = buf }) + vim.api.nvim_set_option_value("buftype", "acwrite", { buf = buf }) + vim.api.nvim_set_option_value("swapfile", false, { buf = buf }) + vim.api.nvim_set_option_value("bufhidden", "delete", { buf = buf }) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { + "// Write your json here.", + "// Writing and quitting (:wq), will generate go struct under the cursor.", + "", + "", + }) + + vim.api.nvim_create_autocmd("BufLeave", { buffer = buf, command = "stopinsert" }) + vim.api.nvim_create_autocmd("BufWriteCmd", { + buffer = buf, + once = true, + callback = function() + local input = vim.api.nvim_buf_get_lines(buf, 0, -1, true) + local inp = table.concat( + vim + .iter(input) + :filter(function(line) + local found = string.find(line, "^//.*") + return (not found) or (line == "") + end) + :totable(), + "\n" + ) + + local go_type = json2go.transform(inp) + if not go_type then + error "cound't convert json to go type" + end + + vim.api.nvim_set_option_value("modified", false, { buf = buf }) + apply(obuf, ocpos, go_type) + + vim.api.nvim_set_current_win(owin) + vim.api.nvim_win_set_cursor(owin, { ocpos + 1, 0 }) + + vim.schedule(function() + pcall(vim.api.nvim_win_close, win, true) + pcall(vim.api.nvim_buf_delete, buf, {}) + end) + end, + }) + + vim.cmd "normal! G" + vim.cmd "startinsert" +end + +--- Converts json string to go type, and puts result under the cursor. If +--- [json_str] is nil, will open an interactive prompt (with cmd set in +--- config). +--- +---@param json_str? string +function json2go.json2go(json_str) + local cur_line = vim.api.nvim_win_get_cursor(0)[1] + if not json_str then + interactive(cur_line) + return + end + + local go_type = json2go.transform(json_str) + if not go_type then + error "cound't convert json to go type" + end + + apply(0, cur_line, go_type) +end + +return json2go diff --git a/lua/gopher/struct_tags.lua b/lua/gopher/struct_tags.lua index 27cfe39..9e7094c 100644 --- a/lua/gopher/struct_tags.lua +++ b/lua/gopher/struct_tags.lua @@ -1,120 +1,200 @@ -local Job = require "plenary.job" -local ts_utils = require "gopher._utils.ts" -local u = require "gopher._utils" -local c = require("gopher.config").config.commands -local M = {} +---@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 +--- How to add/remove/clear 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 +--- 4. Run `:GoTagClear` to clear all tags from struct fields +--- +--- If you want to add/remove tag with options, you can use `json=omitempty` +--- (where json is tag, and omitempty is its option). +--- Example: `:GoTagAdd xml json=omitempty` +--- +--- +--- NOTE: if you dont specify the tag it will use `json` as default +--- +--- Example: +--- >go +--- // before +--- type User struct { +--- // ^ put your cursor here +--- // run `:GoTagAdd yaml` +--- ID int +--- Name string +--- } +--- +--- // after +--- type User struct { +--- ID int `yaml:id` +--- Name string `yaml:name` +--- } +--- < -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 or ns == {} then - return - end +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 = {} + +---@dochide +---@class gopher.StructTagInput +---@field input string[] User provided tags +---@field range? gopher.StructTagRange (optional) + +---@dochide +---@class gopher.StructTagRange +---@field start number +---@field end_ number + +---@param fpath string +---@param bufnr integer +---@param range? gopher.StructTagRange +---@param user_args string[] +---@dochide +local function handle_tags(fpath, bufnr, range, 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) + -- `-struct` and `-line` cannot be combined, setting them separately + if range or st.is_varstruct then + table.insert(cmd, "-line") + table.insert(cmd, string.format("%d,%d", (range or st).start, (range or st).end_)) 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 - -- get result of "gomodifytags" works - local res_data - Job - :new({ - command = c.gomodifytags, - args = cmd_args, - on_exit = function(data, retval) - if retval ~= 0 then - u.notify( - "command 'gomodifytags " .. unpack(cmd_args) .. "' exited with code " .. retval, - "error" - ) - return - end - - res_data = data:result() - end, - }) - :sync() - - -- decode goted value - local tagged = vim.json.decode(table.concat(res_data)) - if - tagged.errors ~= nil - or tagged.lines == nil - or tagged["start"] == nil - or tagged["start"] == 0 - then - u.notify("failed to set tags " .. vim.inspect(tagged), "error") + 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 - for i, v in ipairs(tagged.lines) do - tagged.lines[i] = u.rtrim(v) + for i, v in ipairs(res["lines"]) do + res["lines"][i] = u.trimend(v) end - -- write goted tags 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 ... unknown -function M.add(...) - local arg = { ... } - if #arg == nil or arg == "" then - arg = { "json" } - end - - local cmd_args = { "-add-tags" } - for _, v in ipairs(arg) do - table.insert(cmd_args, v) - end - - modify(unpack(cmd_args)) +---@dochide +---@param option string +local function option_to_tag(option) + return option:match "^(.-)=" end ----remove tags to struct under cursor ----@param ... unknown -function M.remove(...) - local arg = { ... } - if #arg == nil or arg == "" then - arg = { "json" } +---@dochide +---@param args string[] +local function unwrap_if_needed(args) + local out = {} + for _, v in pairs(args) do + for _, p in pairs(vim.split(v, ",")) do + table.insert(out, p) + end end - - local cmd_args = { "-remove-tags" } - for _, v in ipairs(arg) do - table.insert(cmd_args, v) - end - - modify(unpack(cmd_args)) + return out end -return M +---@dochide +---@class gopher.StructTagsArgs +---@field tags string +---@field options string + +---@dochide +---@param args string[] +---@return gopher.StructTagsArgs +function struct_tags.parse_args(args) + args = unwrap_if_needed(args) + + local tags, options = {}, {} + for _, v in pairs(args) do + if string.find(v, "=") then + table.insert(options, v) + table.insert(tags, option_to_tag(v)) + else + table.insert(tags, v) + end + end + + return { + tags = table.concat(u.list_unique(tags), ","), + options = table.concat(u.list_unique(options), ","), + } +end + +-- Adds tags to a struct under the cursor. See `:h gopher.nvim-struct-tags`. +---@param opts gopher.StructTagInput +---@dochide +function struct_tags.add(opts) + log.debug("adding tags", opts) + + local fpath = vim.fn.expand "%" + local bufnr = vim.api.nvim_get_current_buf() + + local user_args = struct_tags.parse_args(opts.input) + handle_tags(fpath, bufnr, opts.range, { + "-add-tags", + (user_args.tags ~= "") and user_args.tags or c.gotag.default_tag, + (user_args.options ~= "" or c.gotag.option) and "-add-options" or nil, + (user_args.options ~= "") and user_args.options or c.gotag.option, + }) +end + +-- Removes tags from a struct under the cursor. See `:h gopher.nvim-struct-tags`. +---@dochide +---@param opts gopher.StructTagInput +function struct_tags.remove(opts) + log.debug("removing tags", opts) + + local fpath = vim.fn.expand "%" + local bufnr = vim.api.nvim_get_current_buf() + + local user_args = struct_tags.parse_args(opts.input) + handle_tags(fpath, bufnr, opts.range, { + "-remove-tags", + (user_args.tags ~= "") and user_args.tags or c.gotag.default_tag, + (user_args.options ~= "" or c.gotag.option ~= nil) and "-remove-options" or nil, + (user_args.options ~= "") and user_args.options or c.gotag.option, + }) +end + +-- 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, nil, { "-clear-tags" }) +end + +return struct_tags diff --git a/nvim.toml b/nvim.toml index fa09a88..b0fce0c 100644 --- a/nvim.toml +++ b/nvim.toml @@ -1,42 +1,5 @@ [vim] any = true -[describe] +[MiniTest] any = true -[[describe.args]] -type = "string" -[[describe.args]] -type = "function" - -[it] -any = true -[[it.args]] -type = "string" -[[it.args]] -type = "function" - -[before_each] -any = true -[[before_each.args]] -type = "function" -[[after_each.args]] -type = "function" - -[assert] -any = true - -[assert.is_not] -any = true - -[[assert.equals.args]] -type = "any" -[[assert.equals.args]] -type = "any" -[[assert.equals.args]] -type = "any" -required = false - -[[assert.same.args]] -type = "any" -[[assert.same.args]] -type = "any" diff --git a/pkg.json b/pkg.json new file mode 100644 index 0000000..7e2add3 --- /dev/null +++ b/pkg.json @@ -0,0 +1,10 @@ +{ + "name": "gopher.nvim", + "engines": { + "nvim": "^0.10.0" + }, + "repository": { + "type": "git", + "url": "https://github.com/olexsmir/gopher.nvim" + } +} diff --git a/plugin/gopher.lua b/plugin/gopher.lua new file mode 100644 index 0000000..5b071b0 --- /dev/null +++ b/plugin/gopher.lua @@ -0,0 +1,107 @@ +---@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|"*"|"?" +---@param range? boolean +---@private +local function cmd(name, fn, nargs, range) + vim.api.nvim_create_user_command(name, fn, { + nargs = nargs or 0, + range = range or false, + }) +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 { + input = opts.fargs, + range = (opts.count ~= -1) and { + start = opts.line1, + end_ = opts.line2, + } or nil, + } +end, "*", true) + +cmd("GoTagRm", function(opts) + require("gopher").tags.rm { + input = opts.fargs, + range = (opts.count ~= -1) and { + start = opts.line1, + end_ = opts.line2, + } or nil, + } +end, "*", true) + +cmd("GoTagClear", function() + require("gopher").tags.clear() +end) + +-- :GoJson +cmd("GoJson", function(opts) + local inp = ((opts.args ~= "" and opts.args) or nil) + require("gopher.json2go").json2go(inp) +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) + 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 942cc04..0000000 --- a/plugin/gopher.vim +++ /dev/null @@ -1,11 +0,0 @@ -command! -nargs=* GoTagAdd :lua require"gopher".tags_add() -command! -nargs=* GoTagRm :lua require"gopher".tags_rm() -command! -nargs=* GoTestAdd :lua require"gopher".test_add() -command! -nargs=* GoTestsAll :lua require"gopher".tests_all() -command! -nargs=* GoTestsExp :lua require"gopher".test_exported() -command! -nargs=* GoMod :lua require"gopher".mod() -command! -nargs=* GoGet :lua require"gopher".get() -command! -nargs=* GoImpl :lua require"gopher".impl() -command! -nargs=* GoGenerate :lua require"gopher".generate() -command! GoCmt :lua require"gopher".comment() -command! GoInstallDeps :lua require"gopher".install_deps() diff --git a/scripts/docgen.lua b/scripts/docgen.lua new file mode 100644 index 0000000..48d906b --- /dev/null +++ b/scripts/docgen.lua @@ -0,0 +1,38 @@ +---@diagnostic disable: undefined-global +--# selene: allow(undefined_variable) + +local okay, minidoc = pcall(require, "mini.doc") +if not okay then + error "mini.doc not found, please install it. https://github.com/echasnovski/mini.doc" + return +end + +local files = { + "lua/gopher/init.lua", + "lua/gopher/config.lua", + "plugin/gopher.lua", + "lua/gopher/struct_tags.lua", + "lua/gopher/json2go.lua", + "lua/gopher/impl.lua", + "lua/gopher/gotests.lua", + "lua/gopher/iferr.lua", + "lua/gopher/comment.lua", +} + +minidoc.setup() + +local hooks = vim.deepcopy(minidoc.default_hooks) +hooks.write_pre = function(lines) + -- Remove first two lines with `======` and `------` delimiters to comply + -- with `:h local-additions` template + table.remove(lines, 1) + table.remove(lines, 1) + + 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 new file mode 100644 index 0000000..bfba744 --- /dev/null +++ b/scripts/minimal_init.lua @@ -0,0 +1,72 @@ +local function root(p) + local f = debug.getinfo(1, "S").source:sub(2) + return vim.fn.fnamemodify(f, ":p:h:h") .. "/" .. (p or "") +end + +local function install_plug(plugin) + local name = plugin:match ".*/(.*)" + local package_root = root ".tests/site/pack/deps/start/" + if not vim.uv.fs_stat(package_root .. name) then + print("Installing " .. plugin) + vim + .system({ + "git", + "clone", + "--depth=1", + "https://github.com/" .. plugin .. ".git", + package_root .. "/" .. name, + }) + :wait() + end +end + +install_plug "nvim-treesitter/nvim-treesitter" +install_plug "echasnovski/mini.doc" -- used for docs generation +install_plug "folke/tokyonight.nvim" -- theme for generating demos +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.o.swapfile = false +vim.o.writebackup = false +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 + +-- set colorscheme only when running ui +if #vim.api.nvim_list_uis() == 1 then + vim.cmd.colorscheme "tokyonight-night" + vim.o.cursorline = true + vim.o.number = true +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/selene.toml b/selene.toml index 8117799..9f6f1c4 100644 --- a/selene.toml +++ b/selene.toml @@ -1 +1,2 @@ -std="nvim+lua51" +std = "nvim+lua52" +exclude = [".tests/*"] 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_many_method_input.go b/spec/fixtures/comment/interface_many_method_input.go new file mode 100644 index 0000000..e5dca33 --- /dev/null +++ b/spec/fixtures/comment/interface_many_method_input.go @@ -0,0 +1,6 @@ +package main + +type Testinger interface { + Get(id string) int + Set(id string, val int) +} diff --git a/spec/fixtures/comment/interface_many_method_output.go b/spec/fixtures/comment/interface_many_method_output.go new file mode 100644 index 0000000..cdeb97c --- /dev/null +++ b/spec/fixtures/comment/interface_many_method_output.go @@ -0,0 +1,7 @@ +package main + +type Testinger interface { + Get(id string) int + // Set + Set(id string, val int) +} diff --git a/spec/fixtures/comment/interface_method_input.go b/spec/fixtures/comment/interface_method_input.go new file mode 100644 index 0000000..7e39a39 --- /dev/null +++ b/spec/fixtures/comment/interface_method_input.go @@ -0,0 +1,5 @@ +package main + +type Testinger interface { + Method(input string) error +} diff --git a/spec/fixtures/comment/interface_method_output.go b/spec/fixtures/comment/interface_method_output.go new file mode 100644 index 0000000..5fa2388 --- /dev/null +++ b/spec/fixtures/comment/interface_method_output.go @@ -0,0 +1,6 @@ +package main + +type Testinger interface { + // Method + Method(input string) error +} 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/many_structs_fields_input.go b/spec/fixtures/comment/many_structs_fields_input.go new file mode 100644 index 0000000..f5c6367 --- /dev/null +++ b/spec/fixtures/comment/many_structs_fields_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/comment/many_structs_fields_output.go b/spec/fixtures/comment/many_structs_fields_output.go new file mode 100644 index 0000000..648f39f --- /dev/null +++ b/spec/fixtures/comment/many_structs_fields_output.go @@ -0,0 +1,19 @@ +package main + +type ( + TestOne struct { + Asdf string + ID int + } + + TestTwo struct { + // Fesa + Fesa int + A bool + } + + TestThree struct { + Asufj int + Fs string + } +) 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_fields_input.go b/spec/fixtures/comment/struct_fields_input.go new file mode 100644 index 0000000..6d6da38 --- /dev/null +++ b/spec/fixtures/comment/struct_fields_input.go @@ -0,0 +1,7 @@ +package main + +type CommentStruct struct { + Name string + Address string + Aliases []string +} diff --git a/spec/fixtures/comment/struct_fields_output.go b/spec/fixtures/comment/struct_fields_output.go new file mode 100644 index 0000000..06e6483 --- /dev/null +++ b/spec/fixtures/comment/struct_fields_output.go @@ -0,0 +1,8 @@ +package main + +type CommentStruct struct { + Name string + // Address + Address string + Aliases []string +} 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/comment/svar_input.go b/spec/fixtures/comment/svar_input.go new file mode 100644 index 0000000..fb75949 --- /dev/null +++ b/spec/fixtures/comment/svar_input.go @@ -0,0 +1,5 @@ +package main + +func varTest() { + s := "something" +} diff --git a/spec/fixtures/comment/svar_output.go b/spec/fixtures/comment/svar_output.go new file mode 100644 index 0000000..e8c917a --- /dev/null +++ b/spec/fixtures/comment/svar_output.go @@ -0,0 +1,6 @@ +package main + +func varTest() { + // s + s := "something" +} diff --git a/spec/fixtures/comment/var_input.go b/spec/fixtures/comment/var_input.go new file mode 100644 index 0000000..47f8257 --- /dev/null +++ b/spec/fixtures/comment/var_input.go @@ -0,0 +1,5 @@ +package main + +func test() { + var imAVar string +} diff --git a/spec/fixtures/comment/var_output.go b/spec/fixtures/comment/var_output.go new file mode 100644 index 0000000..e38aba1 --- /dev/null +++ b/spec/fixtures/comment/var_output.go @@ -0,0 +1,6 @@ +package main + +func test() { + // imAVar + var imAVar string +} diff --git a/spec/fixtures/comment/var_struct_fields_input.go b/spec/fixtures/comment/var_struct_fields_input.go new file mode 100644 index 0000000..c686212 --- /dev/null +++ b/spec/fixtures/comment/var_struct_fields_input.go @@ -0,0 +1,8 @@ +package main + +func main() { + var s struct { + API string + Key string + } +} diff --git a/spec/fixtures/comment/var_struct_fields_output.go b/spec/fixtures/comment/var_struct_fields_output.go new file mode 100644 index 0000000..9676923 --- /dev/null +++ b/spec/fixtures/comment/var_struct_fields_output.go @@ -0,0 +1,9 @@ +package main + +func main() { + var s struct { + API string + // Key + Key string + } +} 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/json2go/interativly_input.go b/spec/fixtures/json2go/interativly_input.go new file mode 100644 index 0000000..c9ecbf5 --- /dev/null +++ b/spec/fixtures/json2go/interativly_input.go @@ -0,0 +1,2 @@ +package main + diff --git a/spec/fixtures/json2go/interativly_output.go b/spec/fixtures/json2go/interativly_output.go new file mode 100644 index 0000000..3314555 --- /dev/null +++ b/spec/fixtures/json2go/interativly_output.go @@ -0,0 +1,5 @@ +package main + +type AutoGenerated struct { + Json bool `json:"json"` +} diff --git a/spec/fixtures/json2go/manual_input.go b/spec/fixtures/json2go/manual_input.go new file mode 100644 index 0000000..c9ecbf5 --- /dev/null +++ b/spec/fixtures/json2go/manual_input.go @@ -0,0 +1,2 @@ +package main + diff --git a/spec/fixtures/json2go/manual_output.go b/spec/fixtures/json2go/manual_output.go new file mode 100644 index 0000000..bd5ac1c --- /dev/null +++ b/spec/fixtures/json2go/manual_output.go @@ -0,0 +1,7 @@ +package main + +type AutoGenerated struct { + User struct { + Name string `json:"name"` + } `json:"user"` +} 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/add_range_input.go b/spec/fixtures/tags/add_range_input.go new file mode 100644 index 0000000..7786518 --- /dev/null +++ b/spec/fixtures/tags/add_range_input.go @@ -0,0 +1,14 @@ +package main + +type Test struct { + ID int + Name string + Num int64 + Cost int + Thingy []string + Testing int + Another struct { + First int + Second string + } +} diff --git a/spec/fixtures/tags/add_range_output.go b/spec/fixtures/tags/add_range_output.go new file mode 100644 index 0000000..2d287ab --- /dev/null +++ b/spec/fixtures/tags/add_range_output.go @@ -0,0 +1,14 @@ +package main + +type Test struct { + ID int + Name string `gopher:"name"` + Num int64 `gopher:"num"` + Cost int `gopher:"cost"` + Thingy []string + Testing int + Another struct { + First int + Second string + } +} 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/overwrite_default_option_input.go b/spec/fixtures/tags/overwrite_default_option_input.go new file mode 100644 index 0000000..89289be --- /dev/null +++ b/spec/fixtures/tags/overwrite_default_option_input.go @@ -0,0 +1,8 @@ +package main + +type Test struct { + ID int + Another struct { + Second string + } +} diff --git a/spec/fixtures/tags/overwrite_default_option_output.go b/spec/fixtures/tags/overwrite_default_option_output.go new file mode 100644 index 0000000..86ab89f --- /dev/null +++ b/spec/fixtures/tags/overwrite_default_option_output.go @@ -0,0 +1,8 @@ +package main + +type Test struct { + ID int `xml:"id,otheroption"` + Another struct { + Second string `xml:"second,otheroption"` + } `xml:"another,otheroption"` +} diff --git a/spec/fixtures/tags/remove_range_input.go b/spec/fixtures/tags/remove_range_input.go new file mode 100644 index 0000000..efa4c9d --- /dev/null +++ b/spec/fixtures/tags/remove_range_input.go @@ -0,0 +1,14 @@ +package main + +type Test struct { + ID int `asdf:"id"` + Name string `asdf:"name"` + Num int64 `asdf:"num"` + Cost int `asdf:"cost"` + Thingy []string `asdf:"thingy"` + Testing int `asdf:"testing"` + Another struct { + First int `asdf:"first"` + Second string `asdf:"second"` + } `asdf:"another"` +} diff --git a/spec/fixtures/tags/remove_range_output.go b/spec/fixtures/tags/remove_range_output.go new file mode 100644 index 0000000..8c51c55 --- /dev/null +++ b/spec/fixtures/tags/remove_range_output.go @@ -0,0 +1,14 @@ +package main + +type Test struct { + ID int `asdf:"id"` + Name string `asdf:"name"` + Num int64 + Cost int + Thingy []string + Testing int `asdf:"testing"` + Another struct { + First int `asdf:"first"` + Second string `asdf:"second"` + } `asdf:"another"` +} diff --git a/spec/fixtures/tags/remove_with_option_input.go b/spec/fixtures/tags/remove_with_option_input.go new file mode 100644 index 0000000..e06d6a5 --- /dev/null +++ b/spec/fixtures/tags/remove_with_option_input.go @@ -0,0 +1,11 @@ +package main + +type Test struct { + ID int `json:"id,omitempty" xml:"id,someoption"` + Name string `json:"name,omitempty" xml:"name,someoption"` + Num int64 `json:"num,omitempty" xml:"num,someoption"` + Another struct { + First int `json:"first,omitempty" xml:"first,someoption"` + Second string `json:"second,omitempty" xml:"second,someoption"` + } `json:"another,omitempty" xml:"another,someoption"` +} diff --git a/spec/fixtures/tags/remove_with_option_output.go b/spec/fixtures/tags/remove_with_option_output.go new file mode 100644 index 0000000..93ed8ae --- /dev/null +++ b/spec/fixtures/tags/remove_with_option_output.go @@ -0,0 +1,11 @@ +package main + +type Test struct { + ID int `xml:"id,someoption"` + Name string `xml:"name,someoption"` + Num int64 `xml:"num,someoption"` + Another struct { + First int `xml:"first,someoption"` + Second string `xml:"second,someoption"` + } `xml:"another,someoption"` +} 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/tags/with_default_option_input.go b/spec/fixtures/tags/with_default_option_input.go new file mode 100644 index 0000000..89289be --- /dev/null +++ b/spec/fixtures/tags/with_default_option_input.go @@ -0,0 +1,8 @@ +package main + +type Test struct { + ID int + Another struct { + Second string + } +} diff --git a/spec/fixtures/tags/with_default_option_output.go b/spec/fixtures/tags/with_default_option_output.go new file mode 100644 index 0000000..6db779c --- /dev/null +++ b/spec/fixtures/tags/with_default_option_output.go @@ -0,0 +1,8 @@ +package main + +type Test struct { + ID int `xml:"id,theoption"` + Another struct { + Second string `xml:"second,theoption"` + } `xml:"another,theoption"` +} diff --git a/spec/fixtures/tags/with_option_input.go b/spec/fixtures/tags/with_option_input.go new file mode 100644 index 0000000..7e27a27 --- /dev/null +++ b/spec/fixtures/tags/with_option_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/with_option_output.go b/spec/fixtures/tags/with_option_output.go new file mode 100644 index 0000000..3fbbabf --- /dev/null +++ b/spec/fixtures/tags/with_option_output.go @@ -0,0 +1,11 @@ +package main + +type Test struct { + ID int `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Num int64 `json:"num,omitempty"` + Another struct { + First int `json:"first,omitempty"` + Second string `json:"second,omitempty"` + } `json:"another,omitempty"` +} 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/gopher_config_spec.lua b/spec/gopher_config_spec.lua deleted file mode 100644 index 1d033c0..0000000 --- a/spec/gopher_config_spec.lua +++ /dev/null @@ -1,41 +0,0 @@ -describe("gopher.config", function() - it("can be required", function() - require "gopher.config" - end) - - it(".setup() when gets empty table not edit config", function() - local c = require "gopher.config" - c.setup {} - - assert.are.same(c.config.commands.go, "go") - assert.are.same(c.config.commands.gomodifytags, "gomodifytags") - assert.are.same(c.config.commands.gotests, "gotests") - assert.are.same(c.config.commands.impl, "impl") - end) - - it(".setup() when get one custom value sets that", function() - local c = require "gopher.config" - c.setup { commands = { - go = "custom_go", - } } - - assert.are.same(c.config.commands.go, "custom_go") - end) - - it(".setup() when get all custom values sets it", function() - local c = require "gopher.config" - c.setup { - commands = { - go = "go1.18", - gomodifytags = "user-gomodifytags", - gotests = "gotests", - impl = "goimpl", - }, - } - - assert.are.same(c.config.commands.go, "go1.18") - assert.are.same(c.config.commands.gomodifytags, "user-gomodifytags") - assert.are.same(c.config.commands.gotests, "gotests") - assert.are.same(c.config.commands.impl, "goimpl") - end) -end) diff --git a/spec/gopher_spec.lua b/spec/gopher_spec.lua deleted file mode 100644 index b50b5ea..0000000 --- a/spec/gopher_spec.lua +++ /dev/null @@ -1,5 +0,0 @@ -describe("gopher", function() - it("can be required", function() - require "gopher" - end) -end) diff --git a/spec/gopher_struct_tags_spec.lua b/spec/gopher_struct_tags_spec.lua deleted file mode 100644 index 826933b..0000000 --- a/spec/gopher_struct_tags_spec.lua +++ /dev/null @@ -1,56 +0,0 @@ -local cur_dir = vim.fn.expand "%:p:h" - -describe("gopher.struct_tags", function() - it("can be required", function() - require "gopher.struct_tags" - end) - - it("can add json tag to struct", function() - local add = require("gopher.struct_tags").add - local name = vim.fn.tempname() .. ".go" - local input_file = vim.fn.readfile(cur_dir .. "/spec/fixtures/tags/add_input.go") - local output_file = vim.fn.join( - vim.fn.readfile(cur_dir .. "/spec/fixtures/tags/add_output.go"), - "\n" - ) - - vim.fn.writefile(input_file, name) - vim.cmd("silent exe 'e " .. name .. "'") - - local bufn = vim.fn.bufnr "" - vim.bo.filetype = "go" - vim.fn.setpos(".", { bufn, 3, 6, 0 }) - add() - - vim.wait(100, function() end) - local fmt = vim.fn.join(vim.fn.readfile(name), "\n") - assert.are.same(output_file, fmt) - - vim.cmd("bd! " .. name) - end) - - it("can remove json tag from struct", function() - local remove = require("gopher.struct_tags").remove - local name = vim.fn.tempname() .. ".go" - local input_file = vim.fn.readfile(cur_dir .. "/spec/fixtures/tags/remove_input.go") - local output_file = vim.fn.join( - vim.fn.readfile(cur_dir .. "/spec/fixtures/tags/remove_output.go"), - "\n" - ) - - vim.fn.writefile(input_file, name) - vim.cmd("silent exe 'e " .. name .. "'") - - local bufn = vim.fn.bufnr "" - vim.bo.filetype = "go" - vim.fn.setpos(".", { bufn, 3, 6, 0 }) - remove() - - vim.wait(100, function() end) - - local fmt = vim.fn.join(vim.fn.readfile(name), "\n") - assert.are.same(output_file, fmt) - - vim.cmd("bd! " .. name) - end) -end) diff --git a/spec/gopher_utils_spec.lua b/spec/gopher_utils_spec.lua deleted file mode 100644 index f052cff..0000000 --- a/spec/gopher_utils_spec.lua +++ /dev/null @@ -1,19 +0,0 @@ -describe("gopher._utils", function() - it("can be requried", function() - require "gopher._utils" - end) - - it(".empty() with non-empty talbe", function() - local empty = require("gopher._utils").empty - local res = empty { first = "1", second = 2 } - - assert.are.same(res, false) - end) - - it(".empty() with empty talbe", function() - local empty = require("gopher._utils").empty - local res = empty {} - - assert.are.same(res, true) - end) -end) diff --git a/spec/integration/comment_test.lua b/spec/integration/comment_test.lua new file mode 100644 index 0000000..e8f54b1 --- /dev/null +++ b/spec/integration/comment_test.lua @@ -0,0 +1,65 @@ +local t = require "spec.testutils" +local child, T, comment = 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 + +comment["should add comment to package"] = function() + do_the_test("package", { 1, 1 }) +end + +comment["should add comment to struct"] = function() + do_the_test("struct", { 4, 1 }) +end + +comment["should add a comment on struct field"] = function() + do_the_test("struct_fields", { 5, 8 }) +end + +comment["should add a comment on var struct field"] = function() + do_the_test("var_struct_fields", { 6, 4 }) +end + +comment["should add a comment on one field of many structs"] = function() + do_the_test("many_structs_fields", { 10, 4 }) +end + +comment["should add comment to function"] = function() + do_the_test("func", { 3, 1 }) +end + +comment["should add comment to method"] = function() + do_the_test("method", { 5, 1 }) +end + +comment["should add comment to interface"] = function() + do_the_test("interface", { 3, 6 }) +end + +comment["should add comment on interface method"] = function() + do_the_test("interface_method", { 4, 2 }) +end + +comment["should add a comment on interface with many method"] = function() + do_the_test("interface_many_method", { 5, 2 }) +end + +comment["should add a comment on a var"] = function() + do_the_test("var", { 4, 2 }) +end + +comment["should add a comment on a short declared var"] = function() + do_the_test("svar", { 4, 8 }) +end + +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..2f2ceb3 --- /dev/null +++ b/spec/integration/gotests_test.lua @@ -0,0 +1,29 @@ +local t = require "spec.testutils" +local child, T, gotests = 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 + +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 + +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..0d88d72 --- /dev/null +++ b/spec/integration/iferr_test.lua @@ -0,0 +1,28 @@ +local t = require "spec.testutils" +local child, T, iferr = t.setup "iferr" + +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 + +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..8cfa11b --- /dev/null +++ b/spec/integration/impl_test.lua @@ -0,0 +1,35 @@ +local t = require "spec.testutils" +local child, T, impl = t.setup "impl" + +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 + +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 + +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/json2go_test.lua b/spec/integration/json2go_test.lua new file mode 100644 index 0000000..83b39ba --- /dev/null +++ b/spec/integration/json2go_test.lua @@ -0,0 +1,25 @@ +local t = require "spec.testutils" +local child, T, json2go = t.setup "json2go" + +json2go["should convert interativly"] = function() + local rs = t.setup_test("json2go/interativly", child, { 2, 0 }) + child.cmd "GoJson" + child.type_keys [[{"json": true}]] + child.type_keys "" + child.cmd "wq" -- quit prompt + child.cmd "write" -- the fixture file + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +json2go["should convert argument"] = function() + local rs = t.setup_test("json2go/manual", child, { 2, 0 }) + child.cmd [[GoJson {"user": {"name": "user-ovic"}}]] + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + 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..9b02c76 --- /dev/null +++ b/spec/integration/struct_tags_test.lua @@ -0,0 +1,147 @@ +local t = require "spec.testutils" +local child, T, struct_tags = t.setup "struct_tags" + +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 + +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 + +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 + +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 + +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 + +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 + +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 + +struct_tags["should add tag with range"] = function() + local rs = t.setup_test("tags/add_range", child, { 5, 1 }) + child.cmd ".,+2GoTagAdd gopher" + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +struct_tags["should remove tag with range"] = function() + local rs = t.setup_test("tags/remove_range", child, { 6, 1 }) + child.cmd ".,+2GoTagRm asdf" + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +struct_tags["should add tags with option"] = function() + local rs = t.setup_test("tags/with_option", child, { 3, 6 }) + child.cmd "GoTagAdd json=omitempty" + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +struct_tags["should add tags with default option"] = function() + child.lua [[ + require("gopher").setup { + gotag = { option = "xml=theoption" }, + } + ]] + + local rs = t.setup_test("tags/with_default_option", child, { 3, 6 }) + child.cmd "GoTagAdd xml" + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +struct_tags["should add tags and overwrite default option"] = function() + child.lua [[ + require("gopher").setup { + gotag = { option = "xml=theoption" }, + } + ]] + + local rs = t.setup_test("tags/overwrite_default_option", child, { 3, 6 }) + child.cmd "GoTagAdd xml=otheroption" + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +struct_tags["should remove tag with specified option"] = function() + local rs = t.setup_test("tags/remove_with_option", child, { 3, 6 }) + child.cmd "GoTagRm json=omitempty" + child.cmd "write" + + t.eq(t.readfile(rs.tmp), rs.fixtures.output) + t.cleanup(rs) +end + +return T diff --git a/spec/minimal.vim b/spec/minimal.vim deleted file mode 100644 index fe175a4..0000000 --- a/spec/minimal.vim +++ /dev/null @@ -1,3 +0,0 @@ -set rtp+=. -packadd plenary.nvim -packadd nvim-treesitter diff --git a/spec/testutils.lua b/spec/testutils.lua new file mode 100644 index 0000000..0b24511 --- /dev/null +++ b/spec/testutils.lua @@ -0,0 +1,108 @@ +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 mod string Module name for which to create a nested test set. +---@return MiniTest.child child nvim client. +---@return table T root test set created by `MiniTest.new_set()`. +---@return table mod_name nested set of tests in `T[mod]`. +function testutils.setup(mod) + 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[mod] = MiniTest.new_set {} + return child, T, T[mod] +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) + vim.validate("pos", pos, "table", true) + + 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 + assert(#pos == 2, "invalid cursor position") + + 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/config_test.lua b/spec/unit/config_test.lua new file mode 100644 index 0000000..0e52d5e --- /dev/null +++ b/spec/unit/config_test.lua @@ -0,0 +1,25 @@ +local t = require "spec.testutils" +local _, T, config = t.setup "config" + +config["can be called without any arguments passed"] = function() + ---@diagnostic disable-next-line: missing-parameter + require("gopher").setup() +end + +config["can be called with empty table"] = function() + ---@diagnostic disable-next-line: missing-fields + require("gopher").setup {} +end + +config["should change option"] = function() + local log_level = 1234567890 + + ---@diagnostic disable-next-line: missing-fields + require("gopher").setup { + log_level = log_level, + } + + t.eq(log_level, require("gopher.config").log_level) +end + +return T diff --git a/spec/unit/struct_tag_test.lua b/spec/unit/struct_tag_test.lua new file mode 100644 index 0000000..6ebc688 --- /dev/null +++ b/spec/unit/struct_tag_test.lua @@ -0,0 +1,68 @@ +local t = require "spec.testutils" +local _, T, st = t.setup "struct_tags" + +st["should parse tags"] = function() + local out = require("gopher.struct_tags").parse_args { "json", "yaml", "etc" } + + t.eq(out.tags, "json,yaml,etc") + t.eq(out.options, "") +end + +st["should parse tags separated by commas"] = function() + local out = require("gopher.struct_tags").parse_args { "json,yaml,etc" } + + t.eq(out.tags, "json,yaml,etc") + t.eq(out.options, "") +end + +st["should parse tags separated by command and spaces"] = function() + local out = require("gopher.struct_tags").parse_args { + "json,yaml", + "json=omitempty", + "xml=something", + } + + t.eq(out.tags, "json,yaml,xml") + t.eq(out.options, "json=omitempty,xml=something") +end + +st["should parse tag with an option"] = function() + local out = require("gopher.struct_tags").parse_args { + "json=omitempty", + "xml", + "xml=theoption", + } + + t.eq(out.tags, "json,xml") + t.eq(out.options, "json=omitempty,xml=theoption") +end + +st["should parse tags with an option"] = function() + local out = require("gopher.struct_tags").parse_args { "json=omitempty", "yaml" } + + t.eq(out.tags, "json,yaml") + t.eq(out.options, "json=omitempty") +end + +st["should parse tags with an option separated with comma"] = function() + local out = require("gopher.struct_tags").parse_args { "json=omitempty,yaml" } + + t.eq(out.tags, "json,yaml") + t.eq(out.options, "json=omitempty") +end + +st["should parse tags with options specified separately"] = function() + local out = require("gopher.struct_tags").parse_args { "json", "yaml", "json=omitempty" } + + t.eq(out.tags, "json,yaml") + t.eq(out.options, "json=omitempty") +end + +st["should parse tags with options specified separately and separated by comma"] = function() + local out = require("gopher.struct_tags").parse_args { "json,yaml,json=omitempty" } + + t.eq(out.tags, "json,yaml") + t.eq(out.options, "json=omitempty") +end + +return T diff --git a/spec/unit/utils_test.lua b/spec/unit/utils_test.lua new file mode 100644 index 0000000..0b71c93 --- /dev/null +++ b/spec/unit/utils_test.lua @@ -0,0 +1,59 @@ +local t = require "spec.testutils" +local _, T, utils = t.setup "utils" + +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 + +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 + +utils["should .trimend()"] = function() + local u = require "gopher._utils" + t.eq(u.trimend " hi ", " hi") +end + +utils["should add .indent() spaces"] = function() + local u = require "gopher._utils" + local line = " func Test() error {" + local indent = 4 + + t.eq(" ", u.indent(line, indent)) +end + +utils["should add .indent() a tab"] = function() + local u = require "gopher._utils" + local line = "\tfunc Test() error {" + local indent = 1 + + t.eq("\t", u.indent(line, indent)) +end + +utils["should add .indent() 2 tabs"] = function() + local u = require "gopher._utils" + local line = "\t\tfunc Test() error {" + local indent = 2 + + t.eq("\t\t", u.indent(line, indent)) +end + +utils["should .list_unique on list with duplicates"] = function() + local u = require "gopher._utils" + t.eq({ "json", "xml" }, u.list_unique { "json", "xml", "json" }) +end + +utils["should .list_unique on list with no duplicates"] = function() + local u = require "gopher._utils" + t.eq({ "json", "xml" }, u.list_unique { "json", "xml" }) +end + +return T diff --git a/vhs/Taskfile.yml b/vhs/Taskfile.yml new file mode 100644 index 0000000..b9cc588 --- /dev/null +++ b/vhs/Taskfile.yml @@ -0,0 +1,23 @@ +version: "3" +tasks: + generate: + deps: + - comment + - iferr + - tags + - impl + + comment: + cmd: vhs comment.tape + + iferr: + cmd: vhs iferr.tape + + tags: + cmd: vhs tags.tape + + impl: + cmd: vhs impl.tape + + json2go: + cmd: vhs json2go.tape diff --git a/vhs/comment.gif b/vhs/comment.gif new file mode 100644 index 0000000..961c477 Binary files /dev/null and b/vhs/comment.gif differ diff --git a/vhs/comment.go b/vhs/comment.go new file mode 100644 index 0000000..3869484 --- /dev/null +++ b/vhs/comment.go @@ -0,0 +1,7 @@ +package demos + +func doSomethingImportant() {} + +type User struct{} + +type DataProvider interface{} diff --git a/vhs/comment.tape b/vhs/comment.tape new file mode 100644 index 0000000..c7dd774 --- /dev/null +++ b/vhs/comment.tape @@ -0,0 +1,29 @@ +Output comment.gif +Require nvim + +Set FontFamily "JetBrains Mono" +Set Height 800 +Set Width 1500 +Set Padding 20 +Set Shell "bash" +Set Theme "tokyonight" +Set TypingSpeed 250ms + +Hide Type@0ms "./nvim.sh comment.go" Enter Show + +# package +Type ":GoCmt" Enter Sleep 500ms Escape Sleep 700ms + +# func +Type@400ms "jjj" +Type ":GoCmt" Enter Sleep 500ms Escape Sleep 700ms + +# struct +Type@400ms "jjj" +Type ":GoCmt" Enter Sleep 500ms Escape Sleep 700ms + +# interface +Type@400ms "jjj" +Type ":GoCmt" Enter Sleep 500ms Escape Sleep 700ms + +Sleep 5s diff --git a/vhs/go.mod b/vhs/go.mod new file mode 100644 index 0000000..bd64a9b --- /dev/null +++ b/vhs/go.mod @@ -0,0 +1,3 @@ +module demos + +go 1.25.0 diff --git a/vhs/iferr.gif b/vhs/iferr.gif new file mode 100644 index 0000000..7bed18e Binary files /dev/null and b/vhs/iferr.gif differ diff --git a/vhs/iferr.go b/vhs/iferr.go new file mode 100644 index 0000000..a1a677d --- /dev/null +++ b/vhs/iferr.go @@ -0,0 +1,11 @@ +package demos + +func ifErr() error { + out, err := doSomething() + + _ = out +} + +func doSomething() (string, error) { + return "", nil +} diff --git a/vhs/iferr.tape b/vhs/iferr.tape new file mode 100644 index 0000000..2658b83 --- /dev/null +++ b/vhs/iferr.tape @@ -0,0 +1,18 @@ +Output iferr.gif +Require nvim +Require iferr + +Set FontFamily "JetBrains Mono" +Set Height 800 +Set Width 1500 +Set Padding 20 +Set Shell "bash" +Set Theme "tokyonight" +Set TypingSpeed 250ms + +Hide Type@0ms "./nvim.sh iferr.go" Enter Show + +Type "3j" +Type ":GoIfErr" Sleep 500ms Enter + +Sleep 5s diff --git a/vhs/impl.gif b/vhs/impl.gif new file mode 100644 index 0000000..cd040a0 Binary files /dev/null and b/vhs/impl.gif differ diff --git a/vhs/impl.go b/vhs/impl.go new file mode 100644 index 0000000..6f6c0ba --- /dev/null +++ b/vhs/impl.go @@ -0,0 +1,3 @@ +package demos + +type CloserExample struct{} diff --git a/vhs/impl.tape b/vhs/impl.tape new file mode 100644 index 0000000..bf0be29 --- /dev/null +++ b/vhs/impl.tape @@ -0,0 +1,18 @@ +Output impl.gif +Require nvim +Require impl + +Set FontFamily "JetBrains Mono" +Set Height 800 +Set Width 1500 +Set Padding 20 +Set Shell "bash" +Set Theme "tokyonight" +Set TypingSpeed 250ms + +Hide Type@0ms "./nvim.sh impl.go" Enter Show + +Type "2j" +Type ":GoImpl c io.Reader" Sleep 700ms Enter + +Sleep 5s diff --git a/vhs/json2go.gif b/vhs/json2go.gif new file mode 100644 index 0000000..908f7dc Binary files /dev/null and b/vhs/json2go.gif differ diff --git a/vhs/json2go.go b/vhs/json2go.go new file mode 100644 index 0000000..c9ecbf5 --- /dev/null +++ b/vhs/json2go.go @@ -0,0 +1,2 @@ +package main + diff --git a/vhs/json2go.tape b/vhs/json2go.tape new file mode 100644 index 0000000..309856e --- /dev/null +++ b/vhs/json2go.tape @@ -0,0 +1,27 @@ +Output json2go.gif +Require nvim +Require json2go + +Set FontFamily "JetBrains Mono" +Set Height 800 +Set Width 1500 +Set Padding 20 +Set Shell "bash" +Set Theme "tokyonight" +Set TypingSpeed 250ms + +Hide Type@0ms "./nvim.sh json2go.go" Enter Show + +Type@0ms "G" +Type@400ms ":GoJson" Sleep 500ms Enter +Type@70ms "{" Enter +Type@70ms `"json": true,` Enter +Type@70ms `"user": {"name": "Name", "of_age": true}` Enter +Type@70ms "}" +Escape Type@500ms ":wq" Enter +Sleep 1s + +Type@25ms "G2o" Escape +Type@120ms `:GoJson {"json": true}` Enter + +Sleep 5s diff --git a/vhs/nvim.sh b/vhs/nvim.sh new file mode 100755 index 0000000..f907eb0 --- /dev/null +++ b/vhs/nvim.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +nvim --clean -u "../scripts/minimal_init.lua" $@ diff --git a/vhs/tags.gif b/vhs/tags.gif new file mode 100644 index 0000000..26d8d86 Binary files /dev/null and b/vhs/tags.gif differ diff --git a/vhs/tags.go b/vhs/tags.go new file mode 100644 index 0000000..91649d7 --- /dev/null +++ b/vhs/tags.go @@ -0,0 +1,12 @@ +package demos + +type AddTagsToMe struct { + Name string + ID int + Address string + Aliases []string + Nested struct { + Foo string + Bar float32 + } +} diff --git a/vhs/tags.tape b/vhs/tags.tape new file mode 100644 index 0000000..d95f76f --- /dev/null +++ b/vhs/tags.tape @@ -0,0 +1,31 @@ +Output tags.gif +Require nvim +Require gomodifytags + +Set FontFamily "JetBrains Mono" +Set Height 800 +Set Width 1500 +Set Padding 20 +Set Shell "bash" +Set Theme "tokyonight" +Set TypingSpeed 250ms + +Hide Type@0ms "./nvim.sh tags.go" Enter Show + +Type@400ms "jjj" +Type ":GoTagAdd json yaml" Sleep 500ms Enter +Type@120ms ":w" Enter +Sleep 1s + +Type ":GoTagRm json" Sleep 500ms Enter +Type ":w" Enter +Sleep 1s + +Type ":GoTagClear" Sleep 500ms Enter +Type ":w" Enter +Sleep 1s + +Type@400ms "jVjj" +Type ":GoTagAdd json=omitempty" Sleep 500ms Enter + +Sleep 5s