diff --git a/pack/acp/start/vim-go/.dockerignore b/pack/acp/start/vim-go/.dockerignore index 286d089..8a4b0bb 100644 --- a/pack/acp/start/vim-go/.dockerignore +++ b/pack/acp/start/vim-go/.dockerignore @@ -4,3 +4,5 @@ .dlv/ .git/ .viminfo +issues/ +autoload/go/**/pkg/ diff --git a/pack/acp/start/vim-go/.github/FUNDING.yml b/pack/acp/start/vim-go/.github/FUNDING.yml new file mode 100644 index 0000000..060026e --- /dev/null +++ b/pack/acp/start/vim-go/.github/FUNDING.yml @@ -0,0 +1 @@ +patreon: bhcleek diff --git a/pack/acp/start/vim-go/.github/ISSUE_TEMPLATE.md b/pack/acp/start/vim-go/.github/ISSUE_TEMPLATE.md index f227239..687b021 100644 --- a/pack/acp/start/vim-go/.github/ISSUE_TEMPLATE.md +++ b/pack/acp/start/vim-go/.github/ISSUE_TEMPLATE.md @@ -1,21 +1,47 @@ -### What did you do? (required. The issue will be **closed** when not provided.) + +### What did you do? (required: The issue will be **closed** when not provided) + + ### What did you expect to happen? - ### What happened instead? - ### Configuration (**MUST** fill this out): -* vim-go version: +#### vim-go version: -* `vimrc` you used to reproduce (use a *minimal* vimrc with other plugins disabled; do not link to a 2,000 line vimrc): +#### `vimrc` you used to reproduce: + + +
vimrc
 
-* Go version (`go version`):
+
-* Go environment (`go env`): +#### Vim version (first three lines from `:version`): + +#### Go version (`go version`): + + +#### Go environment +
go env Output:
+
+
+
+ +#### gopls version +
gopls version Output:
+
+
+
diff --git a/pack/acp/start/vim-go/.github/workflows/test.yml b/pack/acp/start/vim-go/.github/workflows/test.yml new file mode 100644 index 0000000..01b0875 --- /dev/null +++ b/pack/acp/start/vim-go/.github/workflows/test.yml @@ -0,0 +1,60 @@ +name: test +on: [push, pull_request] +jobs: + lint: + name: lint + runs-on: ubuntu-18.04 + steps: + - name: set up python + uses: actions/setup-python@v1.2.0 + with: + python-version: 3.6 + - name: install vim-vint + run: | + python -m pip install --upgrade pip + pip install vim-vint pathlib + - name: checkout + uses: actions/checkout@v2.1.0 + - name: install vim + run: $GITHUB_WORKSPACE/scripts/install-vim vim-8.2 + - name: install tools + run: $GITHUB_WORKSPACE/scripts/install-tools vim-8.2 + - name: lint + run: $GITHUB_WORKSPACE/scripts/lint vim-8.2 + test: + name: test + runs-on: ubuntu-18.04 + strategy: + fail-fast: false + matrix: + go: ['1.14','1.15'] + vim: ['vim-8.0', 'vim-8.2', 'nvim'] + steps: + - name: setup Go + uses: actions/setup-go@v2.0.3 + with: + go-version: ${{ matrix.go }} + - name: set up python + uses: actions/setup-python@v1.2.0 + with: + python-version: 3.6 + - name: install covimerage + run: | + python -m pip install --upgrade pip + pip install covimerage==0.2.1 codecov pathlib + - name: checkout + uses: actions/checkout@v2.1.0 + - name: install vim + run: $GITHUB_WORKSPACE/scripts/install-vim ${{ matrix.vim }} + - name: install tools + run: $GITHUB_WORKSPACE/scripts/install-tools ${{ matrix.vim }} + - name: test + run: $GITHUB_WORKSPACE/scripts/test -c ${{ matrix.vim }} + - uses: codecov/codecov-action@v1 + with: + # token is not required for public repos + #token: ${{ secrets.CODECOV_TOKEN }} + file: $GITHUB_WORKSPACE/coverage.xml + flags: unittests + name: vim-go + fail_ci_if_error: false diff --git a/pack/acp/start/vim-go/.gitignore b/pack/acp/start/vim-go/.gitignore index 5c64849..2f7d833 100644 --- a/pack/acp/start/vim-go/.gitignore +++ b/pack/acp/start/vim-go/.gitignore @@ -1,5 +1,11 @@ -.DS_Store -/doc/tags -/.coverage.covimerage -/coverage.xml *.pyc +.DS_Store +/.bash_history +/.cache +/.config +/.coverage.covimerage +/.local +/.viminfo +/coverage.xml +/doc/tags +/issues diff --git a/pack/acp/start/vim-go/.travis.yml b/pack/acp/start/vim-go/.travis.yml deleted file mode 100644 index 9d7abd6..0000000 --- a/pack/acp/start/vim-go/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: go -go: - - 1.12.1 -notifications: - email: false -matrix: - include: - - env: SCRIPT="test -c" VIM_VERSION=vim-7.4 - - env: SCRIPT="test -c" VIM_VERSION=vim-8.0 - - env: SCRIPT="test -c" VIM_VERSION=nvim - - env: ENV=vimlint SCRIPT=lint VIM_VERSION=vim-8.0 - language: python - python: 3.6 -install: - - ./scripts/install-vim $VIM_VERSION - - | - if [ "$ENV" = "vimlint" ]; then - pip install vim-vint covimerage codecov pathlib - else - pip install --user vim-vint covimerage codecov pathlib - fi -script: - - ./scripts/$SCRIPT $VIM_VERSION diff --git a/pack/acp/start/vim-go/CHANGELOG.md b/pack/acp/start/vim-go/CHANGELOG.md index 09a3a6d..561a18f 100644 --- a/pack/acp/start/vim-go/CHANGELOG.md +++ b/pack/acp/start/vim-go/CHANGELOG.md @@ -1,5 +1,529 @@ ## unplanned +## v1.24 - (September 15, 2020) + +IMPROVEMENTS: +* Clarify how `g:go_imports_autosave` and `g:go_fmt_autosave` interact. + [[GH-2893]](https://github.com/fatih/vim-go/pull/2893) +* Document what the working directory will be for `:GoRun`. + [[GH-2898]](https://github.com/fatih/vim-go/pull/2898) +* Add Ultisnip snippet for wrapping errors. + [[GH-2883]](https://github.com/fatih/vim-go/pull/2883) +* Beautify the godoc pop up window border. + [[GH-2900]](https://github.com/fatih/vim-go/pull/2900) +* Default `g:go_doc_url` to https://pkg.go.dev. + [[GH-2884]](https://github.com/fatih/vim-go/pull/2884) +* Default `g:go_gopls_options` to `[-remote=auto]` to share gopls instances + with other plugins and multiple instances of Vim. + [[GH-2905]](https://github.com/fatih/vim-go/pull/2905) +* Use the module root as the working directory when renaming so that all + references to the symbol will be renamed when in module aware mode and + `g:go_rename_command` is set to `gopls`. + [[GH-2917]](https://github.com/fatih/vim-go/pull/2917) +* Change `g:go_rename_command`'s default to `gopls`. + [[GH-2922]](https://github.com/fatih/vim-go/pull/2922) +* Do not send unnecessary textDocument/didChange notifications to `gopls`. + [[GH-2902]](https://github.com/fatih/vim-go/pull/2902) + [[GH-2930]](https://github.com/fatih/vim-go/pull/2930) +* Stop the debugger when the process being debugged exits. + [[GH-2921]](https://github.com/fatih/vim-go/pull/2921) +* Use the module package cache as a source of packages candidates when trying + to complete package names. + [[GH-2936]](https://github.com/fatih/vim-go/pull/2936) + [[GH-2939]](https://github.com/fatih/vim-go/pull/2939) +* Allow interaction with Vim while waiting for a breakpoint to be hit while + debugging. + [[GH-2932]](https://github.com/fatih/vim-go/pull/2932) +* Refactor Vim signs used for debugging breakpoints to avoid id collision with + other plugins. + [[GH-2943]](https://github.com/fatih/vim-go/pull/2943) +* Refactor debugger's rpc response handling to be asynchronous so that Vim will + be responsive while the program being debugged is executing. + [[GH-2948]](https://github.com/fatih/vim-go/pull/2948) + [[GH-2952]](https://github.com/fatih/vim-go/pull/2952) +* Warn when the debugger breaks in a file that has changed since debugging started. + [[GH-2950]](https://github.com/fatih/vim-go/pull/2950) +* Enable `go-run` mappings that use the terminal to work with Vim in addition to Neovim. + [[GH-2956]](https://github.com/fatih/vim-go/pull/2956) +* Use existing diagnostics for the file when the file hasn't changed and + `g:go_metalinter_command` is `gopls`. + [[GH-2960]](https://github.com/fatih/vim-go/pull/2960) +* Add a new option, `g:go_code_completion_icase`, to allow ignoring case when + filtering completion results. + [[GH-2961]](https://github.com/fatih/vim-go/pull/2961) + * Make sure tools are not cross-compiled with `:GoInstallBinaries` and + `:GoUpdateBinaries`. + [[GH-2982]](https://github.com/fatih/vim-go/pull/2982) + [[GH-2988]](https://github.com/fatih/vim-go/pull/2988) +* Add `:GoDebugHalt` to allow a program being debugged to be paused before it + hits a breakpoint. + [[GH-2983]](https://github.com/fatih/vim-go/pull/2983) +* Clear highlighting of the current line when after resuming when debugging. + [[GH-2984]](https://github.com/fatih/vim-go/pull/2984) +* Add `:GoDebugAttach` to debug a running process. + [[GH-2989]](https://github.com/fatih/vim-go/pull/2989) +* Add `g:go_term_reuse` option to allow the reuse of a terminal window. + [[GH-2990]](https://github.com/fatih/vim-go/pull/2990) +* Add official support for using `gopls`' `gofumpt` workspace setting via + `g:go_gopls_gofumpt`. + [[GH-2994]](https://github.com/fatih/vim-go/pull/2994) + [[GH-3005]](https://github.com/fatih/vim-go/pull/3005) +* Add support for using `gopls`' workspace settings that are otherwise not yet + officially supported by vim-go. + [[GH-2994]](https://github.com/fatih/vim-go/pull/2994) + +BUG FIXES: +* Fix call to non-existent function in terminal mode edge case. + [[GH-2895]](https://github.com/fatih/vim-go/pull/2895) +* Do not show errors when adding a text property for highlighting fails. + [[GH-2892]](https://github.com/fatih/vim-go/pull/2892) +* Include `errcheck` in `g:go_metalinter_enabled`'s default. + [[GH-2903]](https://github.com/fatih/vim-go/pull/2903) +* Fix display of completion selection information on command-line when + `g:go_echo_go_info` is enabled. + [[GH-2907]](https://github.com/fatih/vim-go/pull/2907) +* Prevent `:GoDebugBreakpoint` from causing delve to exit. + [[GH-2908]](https://github.com/fatih/vim-go/pull/2908) +* Use the resolved directory name for `gopls`' working directory when `go.mod` + is in a symlinked path. + [[GH-2913]](https://github.com/fatih/vim-go/pull/2913) +* Fix buffer reuse with `:GoDef`. + [[GH-2928]](https://github.com/fatih/vim-go/pull/2928) +* Handle breakpoints that are already set before calling `:GoDebugStart` or + `:GoDebugTest` in some locales that cause the `sign place` output to vary. + [[GH-2921]](https://github.com/fatih/vim-go/pull/2921) +* Handle diagnostic errors at the end of a .go file. + [[GH-2942]](https://github.com/fatih/vim-go/pull/2942) +* Fix the `go-implements` mapping to use respect `g:go_implements_mode`. + [[GH-2944]](https://github.com/fatih/vim-go/pull/2944) +* Handle null results from `gopls` when getting definitions or type definitions + from virtual files. + [[GH-2951]](https://github.com/fatih/vim-go/pull/2951) +* Fix warning when Neovim is older than v0.4.0. + [[GH-2959]](https://github.com/fatih/vim-go/pull/2959) +* Correct documentation that referred to `g:go_imports_command` to refer to + `g:go_imports_mode` instead. + [[GH-2969]](https://github.com/fatih/vim-go/pull/2969) +* Remove reference to gocode in error message when `g:go_info_mode` is set to + an unsupported value. + [[GH-2978]](https://github.com/fatih/vim-go/pull/2978) +* Make sure debugging commands are configured when debugging a second time + within a single Vim session. + [[GH-2985]](https://github.com/fatih/vim-go/pull/2985) +* Correct documentation in for `:GoModifyTags` when adding a specific tag + value. + [[GH-3001]](https://github.com/fatih/vim-go/pull/3001) +* Fix the path given to `gopls` when `let g:go_metalinter='gopls'` and + `:GoMetaLinter` is called without any arguments. + [[GH-2992]](https://github.com/fatih/vim-go/pull/2992) +* Do not override a user's configuration for `GoDebugBreakpoint` or + `GoDebugCurrent` highlight groups. + [[GH-2998]](https://github.com/fatih/vim-go/pull/2998) +* Apply `gopls` text edits correctly that insert solitary newlines. + [[GH-3000]](https://github.com/fatih/vim-go/pull/3000) + +## v1.23 - (May 16, 2020) + +BACKWARDS INCOMPATIBILITIES: +* Remove support for gocode. + [[GH-2686]](https://github.com/fatih/vim-go/pull/2686) +* Require at least Neovim >= 0.4.0 + [[GH-2853]](https://github.com/fatih/vim-go/pull/2853) + [[GH-2856]](https://github.com/fatih/vim-go/pull/2856) + [[GH-2863]](https://github.com/fatih/vim-go/pull/2863) + +IMPROVEMENTS: +* Make signs for breakpoints configurable. + [[GH-2676]](https://github.com/fatih/vim-go/pull/2676) + [[GH-2690]](https://github.com/fatih/vim-go/pull/2690) +* Enable g:go_gopls_complete_unimported by default to stay aligned with gopls' defaults. + [[GH-2695]](https://github.com/fatih/vim-go/pull/2695) +* Document mappings that were recently added. + [[GH-2699]](https://github.com/fatih/vim-go/pull/2699) +* Handle null arrays better in gopls responses. + [[GH-2703]](https://github.com/fatih/vim-go/pull/2703) +* Use `gopls` defaults by default when they're not otherwise specified in vim-go options. + [[GH-2696]](https://github.com/fatih/vim-go/pull/2696) +* Add support for `gomodifytags --skip-unexported` + [[GH-2660]](https://github.com/fatih/vim-go/pull/2660) +* Show problems that prevent golangci-lint from running linters. + [[GH-2706]](https://github.com/fatih/vim-go/pull/2706) + [[GH-2720]](https://github.com/fatih/vim-go/pull/2720) +* Support golangci-lint config file by not using `--disable-all` when + `g:go_metalinter_enable` or `g:go_metalinter_autosave_enabled` is set to an + empty array. + [[GH-2655]](https://github.com/fatih/vim-go/pull/2655) + [[GH-2715]](https://github.com/fatih/vim-go/pull/2715) +* Add support for Vim8 terminals. + [[GH-2639]](https://github.com/fatih/vim-go/pull/2639) + [[GH-2736]](https://github.com/fatih/vim-go/pull/2736) +* Replace `g:go_gopls_fuzzy_matching` with `g:go_gopls_matcher` in response to + `gopls` deprecation of its `fuzzyMatching` option. + [[GH-2728]](https://github.com/fatih/vim-go/pull/2728) +* Set statuses and progress messages consistently for code quality tools. + [[GH-2727]](https://github.com/fatih/vim-go/pull/2727) +* Add a new supported value to `g:go_fmt_command` to format with `gopls`. + [[GH-2729]](https://github.com/fatih/vim-go/pull/2729) + [[GH-2752]](https://github.com/fatih/vim-go/pull/2752) + [[GH-2852]](https://github.com/fatih/vim-go/pull/2852) +* Handle changes to `go test -v` output. + [[GH-2743]](https://github.com/fatih/vim-go/pull/2743) +* Add `g:go_gopls_mod_tempfile` to configure `gopls`' `tempModfile` + configuration. + [[GH-2747]](https://github.com/fatih/vim-go/pull/2747) +* Add `g:go_gopls_options` to configure `gopls`' commandline options. + [[GH-2747]](https://github.com/fatih/vim-go/pull/2747) +* Improve readability of gopls logs. + [[GH-2773]](https://github.com/fatih/vim-go/pull/2773) +* Introduce `g:go_implements_mode` to allow `:GoImplements` to be satisfied + with `gopls`. + [[GH-2741]](https://github.com/fatih/vim-go/pull/2741) + [[GH-2799]](https://github.com/fatih/vim-go/pull/2799) +* Introduce `g:go_imports_mode` to allow `:GoImports` to be satisfied with + `gopls`. + [[GH-2791]](https://github.com/fatih/vim-go/pull/2791) + [[GH-2794]](https://github.com/fatih/vim-go/pull/2794) + [[GH-2796]](https://github.com/fatih/vim-go/pull/2796) + [[GH-2848]](https://github.com/fatih/vim-go/pull/2848) +* Send LSP synchronization messages to `gopls` when the file does not yet exist + on disk as long as its directory exists. + [[GH-2805]](https://github.com/fatih/vim-go/pull/2805) +* Run `gogetdoc` in the buffer's directory so that it will work regardless of + the user's working directory in module-aware mode. + [[GH-2804]](https://github.com/fatih/vim-go/pull/2804) +* Add `g:go_gopls_analyses` to support `gopls`' analyses options. + [[GH-2820]](https://github.com/fatih/vim-go/pull/2820) +* Add `g:go_gopls_local` to support `gopls`' local option to control how third + party imports are organized. + [[GH-2821]](https://github.com/fatih/vim-go/pull/2821) +* Use gopls to get documentation and documentation links for identifiers under + the cursor. + [[GH-2822]](https://github.com/fatih/vim-go/pull/2822) + [[GH-2839]](https://github.com/fatih/vim-go/pull/2839) +* Clarify documentation for terminal options. + [[GH-2843]](https://github.com/fatih/vim-go/pull/2843) + +BUG FIXES: +* Use the discovered full path for gopls when renaming. + [[GH-2692]](https://github.com/fatih/vim-go/pull/2692) +* Execute commands correctly on windows when `'shell'` is not cmd.exe + [[GH-2713]](https://github.com/fatih/vim-go/pull/2713) + [[GH-2724]](https://github.com/fatih/vim-go/pull/2724) +* Always execute `errcheck` in the current package's directory. + [[GH-2726]](https://github.com/fatih/vim-go/pull/2726) +* Fix errors when highlighting diagnostics after a `:GoImports`. + [[GH-2746]](https://github.com/fatih/vim-go/pull/2746) +* Preserve errors from formatting when both formatting and metalinting happen + on save. + [[GH-2733]](https://github.com/fatih/vim-go/pull/2733) + [[GH-2810]](https://github.com/fatih/vim-go/pull/2810) +* Preserve ordering of gopls messages in the log. + [[GH-2753]](https://github.com/fatih/vim-go/pull/2753) +* Fix `:GoDef` on windows when `g:go_def_mode` is set to `gopls`. + [[GH-2768]](https://github.com/fatih/vim-go/pull/2768) +* Handle null values from `gopls`. + [[GH-2778]](https://github.com/fatih/vim-go/pull/2778) +* Preserve diagnostics highlights after formatting. + [[GH-2779]](https://github.com/fatih/vim-go/pull/2779) +* Fix the decoding and encoding of multi-byte file paths received from and sent + to `gopls`. + [[GH-2784]](https://github.com/fatih/vim-go/pull/2784) +* Fix `:GoRun` so that it works as expected when the current working directory + is neither in GOPATH nor within a module. + [[GH-2782]](https://github.com/fatih/vim-go/pull/2782) + [[GH-2818]](https://github.com/fatih/vim-go/pull/2818) + [[GH-2842]](https://github.com/fatih/vim-go/pull/2842) +* Use absolute file paths for `:GoRun`'s arguments in terminal mode. + [[GH-2844]](https://github.com/fatih/vim-go/pull/2844) +* Show the command executed by `:GoRun` when `g:go_debug` includes `'shell-commands'`. + [[GH-2785]](https://github.com/fatih/vim-go/pull/2785) + [[GH-2817]](https://github.com/fatih/vim-go/pull/2817) +* Clear the list for formatting errors when `g:go_fmt_command` is `gopls`. + [[GH-2790]](https://github.com/fatih/vim-go/pull/2790) +* Handle text edits from gopls that are only line insertions. + [[GH-2802]](https://github.com/fatih/vim-go/pull/2802) + [[GH-2803]](https://github.com/fatih/vim-go/pull/2803) +* Add `g:go_imports_autosave` so that imports can be adjusted on save when + `g:go_imports_mode` is set to `gopls`. + [[GH-2800]](https://github.com/fatih/vim-go/pull/2800) + [[GH-2858]](https://github.com/fatih/vim-go/pull/2858) +* Correct vim-go's help to correctly identify `g:go_referrer_mode`'s default. + [[GH-2832]](https://github.com/fatih/vim-go/pull/2832) +* Clear the quickfix list when `:GoLint` succeeds. + [[GH-2833]](https://github.com/fatih/vim-go/pull/2833) +* Respect arguments to `:GoDocBrowser`. + [[GH-2822]](https://github.com/fatih/vim-go/pull/2822) +* Use the correct path to documentation for struct fields with `:GoDocBrowser`. + [[GH-2822]](https://github.com/fatih/vim-go/pull/2822) +* Do not try parsing errors from terminal jobs when the working directory has + been removed. + [[GH-2824]](https://github.com/fatih/vim-go/pull/2824) +* Document that `g:go_jump_to_error` apples to running the metalinter on save, + too. + [[GH-2854]](https://github.com/fatih/vim-go/pull/2854) +* Ignore commented out import statements when executing `:GoImport`. + [[GH-2862]](https://github.com/fatih/vim-go/pull/2862) +* Interpret file paths in `go vet` errors relative to the current buffer's + directory. + [[GH-2882]](https://github.com/fatih/vim-go/pull/2882) + +## v1.22 - (January 30, 2020) + +BACKWARDS INCOMPATIBILITIES: +* Drop support for Vim 7.4. The minimum required version of Vim is now 8.0.1453. + [[GH-2495]](https://github.com/fatih/vim-go/pull/2495) + [[GH-2497]](https://github.com/fatih/vim-go/pull/2497) +* Drop support for `gometalinter` + [[GH-2494]](https://github.com/fatih/vim-go/pull/2494) + +IMPROVEMENTS: +* Highlight the `go` keyword in go.mod files. + [[GH-2473]](https://github.com/fatih/vim-go/pull/2473) +* Use echo functions consistently. + [[GH-2458]](https://github.com/fatih/vim-go/pull/2458) +* Add support for managing goroutines in debugger. + [[GH-2463]](https://github.com/fatih/vim-go/pull/2463) + [[GH-2527]](https://github.com/fatih/vim-go/pull/2527) +* Document `g:go_doc_popup_window`. + [[GH-2506]](https://github.com/fatih/vim-go/pull/2506) +* Make `g:go_doc_popup_window=1` work for Neovim, too. + [[GH-2451]](https://github.com/fatih/vim-go/pull/2451) + [[GH-2512]](https://github.com/fatih/vim-go/pull/2512) +* Handle errors jumping to a definition in a file open in another Vim process + better. + [[GH-2518]](https://github.com/fatih/vim-go/pull/2518) +* Improve the UX when the gopls binary is missing. + [[GH-2522]](https://github.com/fatih/vim-go/pull/2522) +* Use gopls instead of guru for `:GoSameIds`. + [[GH-2519]](https://github.com/fatih/vim-go/pull/2519) +* Use gopls instead of guru for `:GoReferrers`. + [[GH-2535]](https://github.com/fatih/vim-go/pull/2535) +* Update documentation for `g:go_addtags_transform`. + [[GH-2541]](https://github.com/fatih/vim-go/pull/2541) +* Install most helper tools in module aware mode. + [[GH-2545]](https://github.com/fatih/vim-go/pull/2545) +* Add a new option, `g:go_referrers_mode` to allow the user to choose whether + to use gopls or guru for finding references. + [[GH-2566]](https://github.com/fatih/vim-go/pull/2566) +* Add options to control how gopls responds to completion requests. + [[GH-2567]](https://github.com/fatih/vim-go/pull/2567) + [[GH-2568]](https://github.com/fatih/vim-go/pull/2568) +* Add syntax highlighting for binary literals. + [[GH-2557]](https://github.com/fatih/vim-go/pull/2557) +* Improve highlighting of invalid numeric literals. + [[GH-2571]](https://github.com/fatih/vim-go/pull/2571) + [[GH-2587]](https://github.com/fatih/vim-go/pull/2587) + [[GH-2589]](https://github.com/fatih/vim-go/pull/2589) + [[GH-2584]](https://github.com/fatih/vim-go/pull/2584) + [[GH-2597]](https://github.com/fatih/vim-go/pull/2597) + [[GH-2599]](https://github.com/fatih/vim-go/pull/2599) +* Add highlighting of sections reported by gopls diagnostics' errors + and warnings. + [[GH-2569]](https://github.com/fatih/vim-go/pull/2569) + [[GH-2643]](https://github.com/fatih/vim-go/pull/2643) +* Make the highlighting of fzf decls configurable. + [[GH-2572]](https://github.com/fatih/vim-go/pull/2572) + [[GH-2579]](https://github.com/fatih/vim-go/pull/2579) +* Support renaming with gopls. + [[GH-2577]](https://github.com/fatih/vim-go/pull/2577) + [[GH-2618]](https://github.com/fatih/vim-go/pull/2618) +* Add an option, `g:go_gopls_enabled`, to allow gopls integration to be + disabled. + [[GH-2605]](https://github.com/fatih/vim-go/pull/2605) + [[GH-2609]](https://github.com/fatih/vim-go/pull/2609) + [[GH-2638]](https://github.com/fatih/vim-go/pull/2638) + [[GH-2640]](https://github.com/fatih/vim-go/pull/2640) +* Add a buffer level option, `b:go_fmt_options`, to control formatting options + per buffer. + [[GH-2613]](https://github.com/fatih/vim-go/pull/2613) +* Use build tags when running `:GoVet`. + [[GH-2615]](https://github.com/fatih/vim-go/pull/2615) +* Add new snippets for UltiSnips. + [[GH-2623]](https://github.com/fatih/vim-go/pull/2623) + [[GH-2627]](https://github.com/fatih/vim-go/pull/2627) +* Expand completions as snippets when `g:go_gopls_use_placeholders` is set. + [[GH-2624]](https://github.com/fatih/vim-go/pull/2624) +* Add a new function, `:GoDiagnostics` and an associated mapping for seeing + `gopls` diagnostics. Because of the performance implications on large + projects, `g:go_diagnostics_enabled` controls whether all diagnostics are + processed or only the diagnostics for the current buffer. + [[GH-2612]](https://github.com/fatih/vim-go/pull/2612) +* Explain how to find and detect multiple copies of vim-go in the FAQ. + [[GH-2632]](https://github.com/fatih/vim-go/pull/2632) +* Update the issue template to ask for the gopls version and + `:GoReportGitHubIssue` to provide it. + [[GH-2630]](https://github.com/fatih/vim-go/pull/2630) +* Use text properties when possible for some highlighting cases. + [[GH-2652]](https://github.com/fatih/vim-go/pull/2652) + [[GH-2662]](https://github.com/fatih/vim-go/pull/2662) + [[GH-2663]](https://github.com/fatih/vim-go/pull/2663) + [[GH-2672]](https://github.com/fatih/vim-go/pull/2672) + [[GH-2678]](https://github.com/fatih/vim-go/pull/2678) + + +BUG FIXES: +* Fix removal of missing directories from gopls workspaces. + [[GH-2507]](https://github.com/fatih/vim-go/pull/2507) +* Change to original window before trying to change directories when term job + ends. + [[GH-2508]](https://github.com/fatih/vim-go/pull/2508) +* Swallow errors when the hover info cannot be determined. + [[GH-2515]](https://github.com/fatih/vim-go/pull/2515) +* Fix errors when trying to debug lsp and hover. + [[GH-2516]](https://github.com/fatih/vim-go/pull/2516) +* Reset environment variables on Vim <= 8.0.1831 . + [[GH-2523]](https://github.com/fatih/vim-go/pull/2523) +* Handle empty results from delve. + [[GH-2526]](https://github.com/fatih/vim-go/pull/2526) +* Do not overwrite `updatetime` when `g:go_auto_sameids` or + `g:go_auto_type_info` is set. + [[GH-2529]](https://github.com/fatih/vim-go/pull/2529) +* Fix example for `g:go_debug_log_output` in docs. + [[GH-2547]](https://github.com/fatih/vim-go/pull/2547) +* Use FileChangedShellPost instead of FileChangedShell so that reload messages + are not hidden. + [[GH-2549]](https://github.com/fatih/vim-go/pull/2549) +* Restore cwd after `:GoTest` when `g:go_term_enabled` is set. + [[GH-2556]](https://github.com/fatih/vim-go/pull/2556) +* Expand struct variable correctly in the variables debug window. + [[GH-2574]](https://github.com/fatih/vim-go/pull/2574) +* Show output from errcheck when there are failures other than problems it can + report. + [[GH-2667]](https://github.com/fatih/vim-go/pull/2667) + +## v1.21 - (September 11, 2019) + +BACKWARDS INCOMPATIBILITIES: +* `g:go_metalinter_disabled` has been removed. + [[GH-2375]](https://github.com/fatih/vim-go/pull/2375) + +IMPROVEMENTS: +* Add a new option, `g:go_code_completion_enabled`, to control whether omnifunc + is set. + [[GH-2229]](https://github.com/fatih/vim-go/pull/2229) +* Use build tags with golangci-lint. + [[GH-2261]](https://github.com/fatih/vim-go/pull/2261) +* Allow debugging of packages outside of GOPATH without a go.mod file. + [[GH-2269]](https://github.com/fatih/vim-go/pull/2269) +* Show which example failed when Example tests fail + [[GH-2277]](https://github.com/fatih/vim-go/pull/2277) +* Show function signature and return types in preview window when autocompleting functions and methods. + [[GH-2289]](https://github.com/fatih/vim-go/pull/2289) +* Improve the user experience when using null modules. + [[GH-2300]](https://github.com/fatih/vim-go/pull/2300) +* Modify `:GoReportGitHubIssue` to include vim-go configuration values + [[GH-2323]](https://github.com/fatih/vim-go/pull/2323) +* Respect `g:go_info_mode='gopls'` in go#complete#GetInfo. + [[GH-2313]](https://github.com/fatih/vim-go/pull/2313) +* Allow `:GoLint`, `:GoErrCheck`, and `:GoDebug` to work in null modules. + [[GH-2335]](https://github.com/fatih/vim-go/pull/2335) +* Change default value for `g:go_info_mode` and `g:go_def_mode` to `'gopls'`. + [[GH-2329]](https://github.com/fatih/vim-go/pull/2329) +* Add a new option, `g:go_doc_popup_window` to optionally use a popup window + for godoc in Vim 8.1.1513 and later. + [[GH-2347]](https://github.com/fatih/vim-go/pull/2347) +* Add `:GoAddWorkspace` function to support multiple workspaces with gopls. + [[GH-2356]](https://github.com/fatih/vim-go/pull/2356) +* Install gopls from its stable package. + [[GH-2360]](https://github.com/fatih/vim-go/pull/2360) +* Disambiguate progress message when initializing gopls. + [[GH-2369]](https://github.com/fatih/vim-go/pull/2369) +* Calculate LSP position correctly when on a line that contains multi-byte + characters before the position. + [[GH-2389]](https://github.com/fatih/vim-go/pull/2389) +* Calculate Vim position correctly from LSP text position. + [[GH-2395]](https://github.com/fatih/vim-go/pull/2395) +* Use the statusline to display gopls initialization status messages and only + echo the statuses when `g:go_echo_command_info` is set. + [[GH-2422]](https://github.com/fatih/vim-go/pull/2422) +* Send configuration to gopls so that build tags will be considered and hover + content won't have documentation. + [[GH-2429]](https://github.com/fatih/vim-go/pull/2429) +* Add a new option, `g:go_term_close_on_exit`, to control whether jobs run in a + terminal window will close the terminal window when the job exits. + [[GH-2409]](https://github.com/fatih/vim-go/pull/2409) +* Allow `g:go_template_file` and `g:go_template_test_files` to reside outside + of vim-go's template directory. + [[GH-2434]](https://github.com/fatih/vim-go/pull/2434) +* Add a new command, `:GoLSPDebugBrowser`, to open a browser to gopls debugging + view. + [[GH-2436]](https://github.com/fatih/vim-go/pull/2436) +* Restart gopls automatically when it is updated via `:GoUpdateBinaries`. + [[GH-2453]](https://github.com/fatih/vim-go/pull/2453) +* Reset `'more'` while installing binaries to avoid unnecessary more prompts. + [[GH-2457]](https://github.com/fatih/vim-go/pull/2457) +* Highlight `%w` as a format specifier (for Go 1.13). + [[GH-2433]](https://github.com/fatih/vim-go/pull/2433) +* Handle changes to Go 1.13's go vet output that gometalinter isn't expecting. + [[GH-2475]](https://github.com/fatih/vim-go/pull/2475) +* Make `golangci-lint` the default value for `g:go_metalinter_command`. + [[GH-2478]](https://github.com/fatih/vim-go/pull/2478) +* Parse compiler errors from Go 1.13 `go vet` correctly. + [[GH-2485]](https://github.com/fatih/vim-go/pull/2485) + +BUG FIXES: +* display info about function and function types whose parameters are + `interface{}` without truncating the function signature. + [[GH-2244]](https://github.com/fatih/vim-go/pull/2244) +* install tools that require GOPATH mode when in module mode. + [[GH-2253]](https://github.com/fatih/vim-go/pull/2253) +* Detect GOPATH when starting `gopls` + [[GH-2255]](https://github.com/fatih/vim-go/pull/2255) +* Handle `gopls` responses in the same window from which the respective request + originated. + [[GH-2266]](https://github.com/fatih/vim-go/pull/2266) +* Show completion matches from gocode. + [[GH-2267]](https://github.com/fatih/vim-go/pull/2267) +* Show the completion preview window. + [[GH-2268]](https://github.com/fatih/vim-go/pull/2268) +* Set the anchor for method documentation correctly. + [[GH-2276]](https://github.com/fatih/vim-go/pull/2276) +* Respect the LSP information for determining where candidate matches start. + [[GH-2291]](https://github.com/fatih/vim-go/pull/2291) +* Restore environment variables with backslashes correctly. + [[GH-2292]](https://github.com/fatih/vim-go/pull/2292) +* Modify handling of gopls output for `:GoInfo` to ensure the value will be + displayed. + [[GH-2311]](https://github.com/fatih/vim-go/pull/2311) +* Run `:GoLint` successfully in null modules. + [[GH-2318]](https://github.com/fatih/vim-go/pull/2318) +* Ensure actions on save work in new buffers that have not yet been persisted to disk. + [[GH-2319]](https://github.com/fatih/vim-go/pull/2319) +* Restore population of information in `:GoReportGitHubIssue`. + [[GH-2312]](https://github.com/fatih/vim-go/pull/2312) +* Do not jump back to the originating window when jumping to definitions with + `g:go_def_mode='gopls'`. + [[GH-2327]](https://github.com/fatih/vim-go/pull/2327) +* Fix getting information about a valid identifier for which gopls returns no + information (e.g. calling `:GoInfo` on a package identifier). + [[GH-2339]](https://github.com/fatih/vim-go/pull/2339) +* Fix tab completion of package names on the cmdline in null modules. + [[GH-2342]](https://github.com/fatih/vim-go/pull/2342) +* Display identifier info correctly when the identifier has no godoc. + [[GH-2373]](https://github.com/fatih/vim-go/pull/2373) +* Fix false positives when saving a buffer and `g:go_metalinter_command` is + `golangci-lint`. + [[GH-2367]](https://github.com/fatih/vim-go/pull/2367) +* Fix `:GoDebugRestart`. + [[GH-2390]](https://github.com/fatih/vim-go/pull/2390) +* Do not execute tests twice in terminal mode. + [[GH-2397]](https://github.com/fatih/vim-go/pull/2397) +* Do not open a new buffer in Neovim when there are compilation errors and + terminal mode is enabled. + [[GH-2401]](https://github.com/fatih/vim-go/pull/2401) +* Fix error due to typo in implementation of `:GoAddWorkspace`. + [[GH-2415]](https://github.com/fatih/vim-go/pull/2401) +* Do not format the file automatically when `g:go_format_autosave` is set and + the file being written is not the current file. + [[GH-2442]](https://github.com/fatih/vim-go/pull/2442) +* Fix `go-debug-stepout` mapping. + [[GH-2464]](https://github.com/fatih/vim-go/pull/2464) +* Handle paths with spaces correctly when executing jobs. + [[GH-2472]](https://github.com/fatih/vim-go/pull/2472) +* Remove a space in the default value for `g:go_debug_log_output`, so that + Delve will start on Windows. + [[GH-2480]](https://github.com/fatih/vim-go/pull/2480) + ## 1.20 - (April 22, 2019) FEATURES: @@ -13,7 +537,7 @@ FEATURES: * New `:GoDefType` command to jump to a type definition from an instance of the type. -BACKWARDS INCOMPATABILITIES: +BACKWARDS INCOMPATIBILITIES: * `g:go_highlight_function_arguments` is renamed to `g:go_highlight_function_parameters` [[GH-2117]](https://github.com/fatih/vim-go/pull/2117) diff --git a/pack/acp/start/vim-go/Dockerfile b/pack/acp/start/vim-go/Dockerfile index 7f03ba7..344d0c8 100644 --- a/pack/acp/start/vim-go/Dockerfile +++ b/pack/acp/start/vim-go/Dockerfile @@ -1,6 +1,6 @@ -FROM golang:1.12.1 +FROM golang:1.15.1 -RUN apt-get update -y && \ +RUN apt-get update -y --allow-insecure-repositories && \ apt-get install -y build-essential curl git libncurses5-dev python3-pip && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* @@ -10,11 +10,18 @@ RUN pip3 install vim-vint RUN useradd -ms /bin/bash -d /vim-go vim-go USER vim-go +COPY scripts/install-vim /vim-go/scripts/install-vim +WORKDIR /vim-go + +RUN scripts/install-vim vim-8.0 +RUN scripts/install-vim vim-8.2 +RUN scripts/install-vim nvim + COPY . /vim-go/ WORKDIR /vim-go -RUN scripts/install-vim vim-7.4 -RUN scripts/install-vim vim-8.0 -RUN scripts/install-vim nvim +RUN scripts/install-tools vim-8.0 +RUN scripts/install-tools vim-8.2 +RUN scripts/install-tools nvim ENTRYPOINT ["make"] diff --git a/pack/acp/start/vim-go/Makefile b/pack/acp/start/vim-go/Makefile index 717eb0e..2807e0d 100644 --- a/pack/acp/start/vim-go/Makefile +++ b/pack/acp/start/vim-go/Makefile @@ -1,4 +1,4 @@ -VIMS ?= vim-7.4 vim-8.0 nvim +VIMS ?= vim-8.0 vim-8.2 nvim all: install lint test @@ -6,6 +6,7 @@ install: @echo "==> Installing Vims: $(VIMS)" @for vim in $(VIMS); do \ ./scripts/install-vim $$vim; \ + ./scripts/install-tools $$vim; \ done test: @@ -16,7 +17,7 @@ test: lint: @echo "==> Running linting tools" - @./scripts/lint vim-8.0 + @./scripts/lint vim-8.2 docker: @echo "==> Building/starting Docker container" diff --git a/pack/acp/start/vim-go/README.md b/pack/acp/start/vim-go/README.md index c47c8f3..157bbea 100644 --- a/pack/acp/start/vim-go/README.md +++ b/pack/acp/start/vim-go/README.md @@ -1,4 +1,6 @@ -# vim-go [![Build Status](http://img.shields.io/travis/fatih/vim-go.svg?style=flat-square)](https://travis-ci.org/fatih/vim-go) +# vim-go [![Build Status](http://img.shields.io/travis/fatih/vim-go.svg?style=flat-square)](https://travis-ci.org/fatih/vim-go) [![GitHub Actions Status](https://github.com/fatih/vim-go/workflows/test/badge.svg)](https://github.com/fatih/vim-go/actions) + +

Vim-go logo @@ -13,15 +15,15 @@ This plugin adds Go language support for Vim, with the following main features: * Quickly execute your current file(s) with `:GoRun`. * Improved syntax highlighting and folding. * Debug programs with integrated `delve` support with `:GoDebugStart`. -* Completion support via `gocode` and `gopls`. -* `gofmt` or `goimports` on save keeps the cursor position and undo history. +* Completion and many other features support via `gopls`. +* formatting on save keeps the cursor position and undo history. * Go to symbol/declaration with `:GoDef`. * Look up documentation with `:GoDoc` or `:GoDocBrowser`. * Easily import packages via `:GoImport`, remove them via `:GoDrop`. * Precise type-safe renaming of identifiers with `:GoRename`. * See which code is covered by tests with `:GoCoverage`. * Add or remove tags on struct fields with `:GoAddTags` and `:GoRemoveTags`. -* Call `gometalinter` with `:GoMetaLinter` to invoke all possible linters +* Call `golangci-lint` with `:GoMetaLinter` to invoke all possible linters (`golint`, `vet`, `errcheck`, `deadcode`, etc.) and put the result in the quickfix or location list. * Lint your code with `:GoLint`, run your code through `:GoVet` to catch static @@ -30,10 +32,12 @@ This plugin adds Go language support for Vim, with the following main features: `:GoCallees`, and `:GoReferrers`. * ... and many more! Please see [doc/vim-go.txt](doc/vim-go.txt) for more information. +* The `gopls` instance can be shared with other Vim plugins. +* Vim-go's use of `gopls` can be disabled. ## Install -vim-go requires at least Vim 7.4.2009 or Neovim 0.3.1. +vim-go requires at least Vim 8.0.1453 or Neovim 0.4.0. The [**latest stable release**](https://github.com/fatih/vim-go/releases/latest) is the recommended version to use. If you choose to use the master branch instead, @@ -68,8 +72,28 @@ Depending on your installation method, you may have to generate the plugin's [`help tags`](http://vimhelp.appspot.com/helphelp.txt.html#%3Ahelptags) manually (e.g. `:helptags ALL`). -We also have an [official vim-go tutorial](https://github.com/fatih/vim-go-tutorial). +We also have an [official vim-go tutorial](https://github.com/fatih/vim-go/wiki). + +## FAQ and troubleshooting + +The FAQ and troubleshooting tips are in the documentation and can be quickly +accessed using `:help go-troubleshooting`. If you believe you've found a bug or +shortcoming in vim-go that is neither addressed by help nor in [existing +issues](https://github.com/fatih/vim-go/issues), please open an issue with +clear reproduction steps. `:GoReportGitHubIssue` can be used pre-populate a lot +of the information needed when creating a new issue. + +## Contributing + +All PRs are welcome. If you are planning to contribute a large patch or to +integrate a new tool, please create an issue first to get any upfront questions +or design decisions out of the way first. + +You can run the tests locally by running `make`. It will lint the VimL for you, +lint the documentation, and run the tests against the minimum required version +of Vim, other versions of Vim that may be critical to support, and Neovim. ## License The BSD 3-Clause License - see [`LICENSE`](LICENSE) for more details + diff --git a/pack/acp/start/vim-go/autoload/fzf/decls.vim b/pack/acp/start/vim-go/autoload/fzf/decls.vim index 20e4f04..02bf462 100644 --- a/pack/acp/start/vim-go/autoload/fzf/decls.vim +++ b/pack/acp/start/vim-go/autoload/fzf/decls.vim @@ -121,10 +121,10 @@ function! s:source(mode,...) abort \ decl.col \) call add(ret_decls, printf("%s\t%s %s\t%s", - \ s:color(decl.ident . space, "Function"), - \ s:color(decl.keyword, "Keyword"), - \ s:color(pos, "SpecialComment"), - \ s:color(decl.full, "Comment"), + \ s:color(decl.ident . space, "goDeclsFzfFunction"), + \ s:color(decl.keyword, "goDeclsFzfKeyword"), + \ s:color(pos, "goDeclsFzfSpecialComment"), + \ s:color(decl.full, "goDeclsFzfComment"), \)) endfor diff --git a/pack/acp/start/vim-go/autoload/go/auto.vim b/pack/acp/start/vim-go/autoload/go/auto.vim index 1174858..4884320 100644 --- a/pack/acp/start/vim-go/autoload/go/auto.vim +++ b/pack/acp/start/vim-go/autoload/go/auto.vim @@ -11,7 +11,43 @@ function! go#auto#template_autocreate() call go#template#create() endfunction -function! go#auto#echo_go_info() +function! go#auto#complete_done() + call s:echo_go_info() + call s:ExpandSnippet() +endfunction + +function! s:ExpandSnippet() abort + if !exists('v:completed_item') || empty(v:completed_item) || !go#config#GoplsUsePlaceholders() + return + endif + + + let l:engine = go#config#SnippetEngine() + + if l:engine is 'ultisnips' + if !get(g:, 'did_plugin_ultisnips', 0) + return + endif + " the snippet may have a '{\}' in it. For UltiSnips, that should be spelled + " \{}. fmt.Printf is such a snippet that can be used to demonstrate. + let l:snippet = substitute(v:completed_item.word, '{\\}', '\{}', 'g') + call UltiSnips#Anon(l:snippet, v:completed_item.word, '', 'i') +" elseif l:engine is 'neosnippet' +" " TODO(bc): make the anonymous expansion for neosnippet work +" +" if !get(g:, 'loaded_neosnippet') is 1 +" return +" endif +" +" " neosnippet#anonymous doesn't need a trigger, so replace the +" " completed_item.word with an empty string before calling neosnippet#anonymous +" let l:snippet = substitute(v:completed_item.word, '{\\}', '\{\}', 'g') +" call setline('.', substitute(getline('.'), substitute(v:completed_item.word, '\', '\\', 'g'), '', '')) +" call neosnippet#anonymous(l:snippet) + endif +endfunction + +function! s:echo_go_info() if !go#config#EchoGoInfo() return endif @@ -21,46 +57,109 @@ function! go#auto#echo_go_info() endif let item = v:completed_item - if !has_key(item, "info") + if !has_key(item, "user_data") return endif - if empty(item.info) + if empty(item.user_data) return endif - redraws! | echo "vim-go: " | echohl Function | echon item.info | echohl None + redraws! | echo "vim-go: " | echohl Function | echon item.user_data | echohl None endfunction -function! go#auto#auto_type_info() - if !go#config#AutoTypeInfo() || !filereadable(expand('%:p')) +let s:timer_id = 0 + +" go#auto#update_autocmd() will be called on BufEnter,CursorHold. This +" configures the augroup according to conditions below. +" +" | # | has_timer | should_enable | do | +" |---|-----------|---------------|------------------------------------| +" | 1 | false | false | return early | +" | 2 | true | true | return early | +" | 3 | true | false | clear the group and stop the timer | +" | 4 | false | true | configure the group | +function! go#auto#update_autocmd() + let has_timer = get(b:, 'has_timer') + let should_enable = go#config#AutoTypeInfo() || go#config#AutoSameids() + if (!has_timer && !should_enable) || (has_timer && should_enable) return endif - " GoInfo automatic update - call go#tool#Info(0) + if has_timer + augroup vim-go-buffer-auto + autocmd! * + augroup END + let b:has_timer = 0 + call s:timer_stop() + return + endif + + augroup vim-go-buffer-auto + autocmd! * + autocmd CursorMoved call s:timer_restart() + autocmd BufLeave call s:timer_stop() + augroup END + let b:has_timer = 1 + call s:timer_start() endfunction -function! go#auto#auto_sameids() - if !go#config#AutoSameids() || !filereadable(expand('%:p')) - return +function! s:timer_restart() + if isdirectory(expand('%:p:h')) + call s:timer_stop() + call s:timer_start() endif +endfunction - " GoSameId automatic update - call go#guru#SameIds(0) +function! s:timer_stop() + if s:timer_id + call timer_stop(s:timer_id) + let s:timer_id = 0 + endif +endfunction + +function! s:timer_start() + let s:timer_id = timer_start(go#config#Updatetime(), function('s:handler')) +endfunction + +function! s:handler(timer_id) + if go#config#AutoTypeInfo() + call go#tool#Info(0) + endif + if go#config#AutoSameids() + call go#guru#SameIds(0) + endif + let s:timer_id = 0 endfunction function! go#auto#fmt_autosave() - if !go#config#FmtAutosave() || !filereadable(expand('%:p')) + if !(isdirectory(expand('%:p:h')) && expand(':p') == expand('%:p')) + return + endif + + if !(go#config#FmtAutosave() || go#config#ImportsAutosave()) + return + endif + + if go#config#ImportsAutosave() && !(go#config#FmtAutosave() && go#config#FmtCommand() == 'goimports') + call go#fmt#Format(1) + + " return early when the imports mode is goimports, because there's no need + " to format again when goimports was run + if go#config#ImportsMode() == 'goimports' + return + endif + endif + + if !go#config#FmtAutosave() return endif - " Go code formatting on save call go#fmt#Format(-1) endfunction function! go#auto#metalinter_autosave() - if !go#config#MetalinterAutosave() || !filereadable(expand('%:p')) + if !go#config#MetalinterAutosave() || !isdirectory(expand('%:p:h')) return endif @@ -69,7 +168,7 @@ function! go#auto#metalinter_autosave() endfunction function! go#auto#modfmt_autosave() - if !go#config#ModFmtAutosave() || !filereadable(expand('%:p')) + if !(go#config#ModFmtAutosave() && isdirectory(expand('%:p:h')) && expand(':p') == expand('%:p')) return endif @@ -78,7 +177,7 @@ function! go#auto#modfmt_autosave() endfunction function! go#auto#asmfmt_autosave() - if !go#config#AsmfmtAutosave() || !filereadable(expand('%:p')) + if !(go#config#AsmfmtAutosave() && isdirectory(expand('%:p:h')) && expand(':p') == expand('%:p')) return endif diff --git a/pack/acp/start/vim-go/autoload/go/cmd.vim b/pack/acp/start/vim-go/autoload/go/cmd.vim index fdf2299..8867166 100644 --- a/pack/acp/start/vim-go/autoload/go/cmd.vim +++ b/pack/acp/start/vim-go/autoload/go/cmd.vim @@ -9,8 +9,8 @@ function! go#cmd#autowrite() abort for l:nr in range(0, bufnr('$')) if buflisted(l:nr) && getbufvar(l:nr, '&modified') " Sleep one second to make sure people see the message. Otherwise it is - " often immediacy overwritten by the async messages (which also don't - " invoke the "hit ENTER" prompt). + " often immediately overwritten by the async messages (which also + " doesn't invoke the "hit ENTER" prompt). call go#util#EchoWarning('[No write since last change]') sleep 1 return @@ -32,7 +32,7 @@ function! go#cmd#Build(bang, ...) abort \ map(copy(a:000), "expand(v:val)") + \ [".", "errors"] - " Vim and Neovim async. + " Vim and Neovim async if go#util#has_job() call s:cmd_job({ \ 'cmd': ['go'] + args, @@ -41,8 +41,15 @@ function! go#cmd#Build(bang, ...) abort \ 'statustype': 'build' \}) - " Vim 7.4 without async + " Vim without async else + let l:status = { + \ 'desc': 'current status', + \ 'type': 'build', + \ 'state': "started", + \ } + call go#statusline#Update(expand('%:p:h'), l:status) + let default_makeprg = &makeprg let &makeprg = "go " . join(go#util#Shelllist(args), ' ') @@ -68,10 +75,16 @@ function! go#cmd#Build(bang, ...) abort call go#list#Window(l:listtype, len(errors)) if !empty(errors) && !a:bang call go#list#JumpToFirst(l:listtype) + let l:status.state = 'failed' else - call go#util#EchoSuccess("[build] SUCCESS") + let l:status.state = 'success' + if go#config#EchoCommandInfo() + call go#util#EchoSuccess("[build] SUCCESS") + endif endif + call go#statusline#Update(expand('%:p:h'), l:status) endif + endfunction @@ -104,16 +117,15 @@ endfunction " Run runs the current file (and their dependencies if any) in a new terminal. function! go#cmd#RunTerm(bang, mode, files) abort - let cmd = "go run " - let tags = go#config#BuildTags() - if len(tags) > 0 - let cmd .= "-tags " . go#util#Shellescape(tags) . " " + let cmd = ["go", "run"] + if len(go#config#BuildTags()) > 0 + call extend(cmd, ["-tags", go#config#BuildTags()]) endif if empty(a:files) - let cmd .= go#util#Shelljoin(go#tool#Files()) + call extend(cmd, go#tool#Files()) else - let cmd .= go#util#Shelljoin(map(copy(a:files), "expand(v:val)"), 1) + call extend(cmd, map(copy(a:files), funcref('s:expandRunArgs'))) endif call go#term#newmode(a:bang, cmd, s:runerrorformat(), a:mode) endfunction @@ -123,7 +135,7 @@ endfunction " suitable for long running apps, because vim is blocking by default and " calling long running apps will block the whole UI. function! go#cmd#Run(bang, ...) abort - if has('nvim') + if go#config#TermEnabled() call go#cmd#RunTerm(a:bang, '', a:000) return endif @@ -134,61 +146,101 @@ function! go#cmd#Run(bang, ...) abort " anything. Once this is implemented we're going to make :GoRun async endif - let cmd = "go run " - let tags = go#config#BuildTags() - if len(tags) > 0 - let cmd .= "-tags " . go#util#Shellescape(tags) . " " + let l:status = { + \ 'desc': 'current status', + \ 'type': 'run', + \ 'state': "started", + \ } + + call go#statusline#Update(expand('%:p:h'), l:status) + + let l:cmd = ['go', 'run'] + let l:tags = go#config#BuildTags() + if len(l:tags) > 0 + let l:cmd = l:cmd + ['-tags', l:tags] endif + if a:0 == 0 + let l:files = go#tool#Files() + else + let l:files = map(copy(a:000), funcref('s:expandRunArgs')) + endif + + let l:cmd = l:cmd + l:files + + let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' + let l:dir = getcwd() + if go#util#IsWin() - if a:0 == 0 - exec '!' . cmd . go#util#Shelljoin(go#tool#Files(), 1) - else - exec '!' . cmd . go#util#Shelljoin(map(copy(a:000), "expand(v:val)"), 1) - endif + try + if go#util#HasDebug('shell-commands') + call go#util#EchoInfo(printf('shell command: %s', string(l:cmd))) + endif + execute l:cd . fnameescape(expand("%:p:h")) + exec printf('!%s', go#util#Shelljoin(l:cmd, 1)) + finally + execute l:cd . fnameescape(l:dir) + endtry + + let l:status.state = 'success' if v:shell_error - redraws! | echon "vim-go: [run] " | echohl ErrorMsg | echon "FAILED"| echohl None + let l:status.state = 'failed' + if go#config#EchoCommandInfo() + redraws! + call go#util#EchoError('[run] FAILED') + endif else - redraws! | echon "vim-go: [run] " | echohl Function | echon "SUCCESS"| echohl None + if go#config#EchoCommandInfo() + redraws! + call go#util#EchoSuccess('[run] SUCCESS') + endif endif + call go#statusline#Update(expand('%:p:h'), l:status) return endif " :make expands '%' and '#' wildcards, so they must also be escaped - let default_makeprg = &makeprg - if a:0 == 0 - let &makeprg = cmd . go#util#Shelljoin(go#tool#Files(), 1) - else - let &makeprg = cmd . go#util#Shelljoin(map(copy(a:000), "expand(v:val)"), 1) - endif + let l:default_makeprg = &makeprg + let &makeprg = go#util#Shelljoin(l:cmd, 1) let l:listtype = go#list#Type("GoRun") - try + let l:status.state = 'success' + try " backup user's errorformat, will be restored once we are finished let l:old_errorformat = &errorformat let &errorformat = s:runerrorformat() + + if go#util#HasDebug('shell-commands') + call go#util#EchoInfo('shell command: ' . l:cmd) + endif + + execute l:cd . fnameescape(expand("%:p:h")) if l:listtype == "locationlist" exe 'lmake!' else exe 'make!' endif finally - "restore errorformat + "restore the working directory, errformat, and makeprg + execute cd . fnameescape(l:dir) let &errorformat = l:old_errorformat - let &makeprg = default_makeprg + let &makeprg = l:default_makeprg endtry let l:errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(l:errors)) - if !empty(l:errors) && !a:bang - call go#list#JumpToFirst(l:listtype) + if !empty(l:errors) + let l:status.state = 'failed' + if !a:bang + call go#list#JumpToFirst(l:listtype) + endif endif - + call go#statusline#Update(expand('%:p:h'), l:status) endfunction " Install installs the package by simple calling 'go install'. If any argument @@ -255,9 +307,18 @@ function! go#cmd#Generate(bang, ...) abort let &makeprg = "go generate " . goargs . ' ' . gofiles endif - let l:listtype = go#list#Type("GoGenerate") + let l:status = { + \ 'desc': 'current status', + \ 'type': 'generate', + \ 'state': "started", + \ } + call go#statusline#Update(expand('%:p:h'), l:status) - echon "vim-go: " | echohl Identifier | echon "generating ..."| echohl None + if go#config#EchoCommandInfo() + call go#util#EchoProgress('generating ...') + endif + + let l:listtype = go#list#Type("GoGenerate") try if l:listtype == "locationlist" @@ -273,13 +334,18 @@ function! go#cmd#Generate(bang, ...) abort let errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(errors)) if !empty(errors) + let l:status.status = 'failed' if !a:bang call go#list#JumpToFirst(l:listtype) endif else - redraws! | echon "vim-go: " | echohl Function | echon "[generate] SUCCESS"| echohl None + let l:status.status = 'success' + if go#config#EchoCommandInfo() + redraws! + call go#util#EchoSuccess('[generate] SUCCESS') + endif endif - + call go#statusline#Update(expand(':%:p:h'), l:status) endfunction function! s:runerrorformat() @@ -288,6 +354,18 @@ function! s:runerrorformat() return l:errorformat endfunction +" s:expandRunArgs expands arguments for go#cmd#Run according to the +" documentation of :GoRun. When val is a readable file, it is expanded to the +" full path so that go run can be executed in the current buffer's directory. +" val is return unaltered otherwise to support non-file arguments to go run. +function! s:expandRunArgs(idx, val) abort + let l:val = expand(a:val) + if !filereadable(l:val) + return l:val + endif + + return fnamemodify(l:val, ':p')") +endfunction " --------------------- " | Vim job callbacks | " --------------------- diff --git a/pack/acp/start/vim-go/autoload/go/complete.vim b/pack/acp/start/vim-go/autoload/go/complete.vim index 987c484..b256e8c 100644 --- a/pack/acp/start/vim-go/autoload/go/complete.vim +++ b/pack/acp/start/vim-go/autoload/go/complete.vim @@ -2,276 +2,48 @@ let s:cpo_save = &cpo set cpo&vim -function! s:gocodeCommand(cmd, args) abort - let l:gocode_bin = "gocode" - let l:gomod = go#util#gomod() - if filereadable(l:gomod) - let l:gocode_bin = "gocode-gomod" - endif - - let bin_path = go#path#CheckBinPath(l:gocode_bin) - if empty(bin_path) - return [] - endif - - let socket_type = go#config#GocodeSocketType() - - let cmd = [bin_path] - let cmd = extend(cmd, ['-sock', socket_type]) - let cmd = extend(cmd, ['-f', 'vim']) - - if go#config#GocodeProposeBuiltins() - let cmd = extend(cmd, ['-builtin']) - endif - - if go#config#GocodeProposeSource() - let cmd = extend(cmd, ['-source']) - else - let cmd = extend(cmd, ['-fallback-to-source', '-cache']) - endif - - if go#config#GocodeUnimportedPackages() - let cmd = extend(cmd, ['-unimported-packages']) - endif - - let cmd = extend(cmd, [a:cmd]) - let cmd = extend(cmd, a:args) - - return cmd -endfunction - -function! s:sync_gocode(cmd, args, input) abort - " We might hit cache problems, as gocode doesn't handle different GOPATHs - " well. See: https://github.com/nsf/gocode/issues/239 - let old_goroot = $GOROOT - let $GOROOT = go#util#env("goroot") - - try - let cmd = s:gocodeCommand(a:cmd, a:args) - " gocode can sometimes be slow, so redraw now to avoid waiting for gocode - " to return before redrawing automatically. - redraw - - let [l:result, l:err] = go#util#Exec(cmd, a:input) - finally - let $GOROOT = old_goroot - endtry - - if l:err != 0 - return "[0, []]" - endif - - if &encoding != 'utf-8' - let l:result = iconv(l:result, 'utf-8', &encoding) - endif - - return l:result -endfunction - -function! s:gocodeAutocomplete() abort - " use the offset as is, because the cursor position is the position for - " which autocomplete candidates are needed. - return s:sync_gocode('autocomplete', - \ [expand('%:p'), go#util#OffsetCursor()], - \ go#util#GetLines()) -endfunction - " go#complete#GoInfo returns the description of the identifier under the " cursor. function! go#complete#GetInfo() abort - return s:sync_info(0) -endfunction - -function! go#complete#Info(showstatus) abort - if go#util#has_job(1) - return s:async_info(1, a:showstatus) - else - return s:sync_info(1) - endif -endfunction - -function! s:async_info(echo, showstatus) - let state = {'echo': a:echo} - - " explicitly bind complete to state so that within it, self will - " always refer to state. See :help Partial for more information. - let state.complete = function('s:complete', [], state) - - " add 1 to the offset, so that the position at the cursor will be included - " in gocode's search - let offset = go#util#OffsetCursor()+1 - - " We might hit cache problems, as gocode doesn't handle different GOPATHs - " well. See: https://github.com/nsf/gocode/issues/239 - let env = { - \ "GOROOT": go#util#env("goroot") - \ } - - let opts = { - \ 'bang': 1, - \ 'complete': state.complete, - \ 'for': '_', - \ } - - if a:showstatus - let opts.statustype = 'gocode' - endif - - let opts = go#job#Options(l:opts) - - let cmd = s:gocodeCommand('autocomplete', - \ [expand('%:p'), offset]) - - " TODO(bc): Don't write the buffer to a file; pass the buffer directly to - " gocode's stdin. It shouldn't be necessary to use {in_io: 'file', in_name: - " s:gocodeFile()}, but unfortunately {in_io: 'buffer', in_buf: bufnr('%')} - " doesn't work. - call extend(opts, { - \ 'env': env, - \ 'in_io': 'file', - \ 'in_name': s:gocodeFile(), - \ }) - - call go#job#Start(cmd, opts) -endfunction - -function! s:complete(job, exit_status, messages) abort dict - if a:exit_status != 0 - return - endif - - if &encoding != 'utf-8' - let i = 0 - while i < len(a:messages) - let a:messages[i] = iconv(a:messages[i], 'utf-8', &encoding) - let i += 1 - endwhile - endif - - let result = s:info_filter(self.echo, join(a:messages, "\n")) - call s:info_complete(self.echo, result) -endfunction - -function! s:gocodeFile() - let file = tempname() - call writefile(go#util#GetLines(), file) - return file -endfunction - -function! s:sync_info(echo) - " add 1 to the offset, so that the position at the cursor will be included - " in gocode's search - let offset = go#util#OffsetCursor()+1 - - let result = s:sync_gocode('autocomplete', - \ [expand('%:p'), offset], - \ go#util#GetLines()) - - let result = s:info_filter(a:echo, result) - return s:info_complete(a:echo, result) -endfunction - -function! s:info_filter(echo, result) abort - if empty(a:result) - return "" - endif - - let l:result = eval(a:result) - if len(l:result) != 2 - return "" - endif - - let l:candidates = l:result[1] - if len(l:candidates) == 1 - " When gocode panics in vim mode, it returns - " [0, [{'word': 'PANIC', 'abbr': 'PANIC PANIC PANIC', 'info': 'PANIC PANIC PANIC'}]] - if a:echo && l:candidates[0].info ==# "PANIC PANIC PANIC" - return "" - endif - - return l:candidates[0].info - endif - - let filtered = [] - let wordMatch = '\<' . expand("") . '\>' - " escape single quotes in wordMatch before passing it to filter - let wordMatch = substitute(wordMatch, "'", "''", "g") - let filtered = filter(l:candidates, "v:val.info =~ '".wordMatch."'") - - if len(l:filtered) == 0 - return "no matches" - elseif len(l:filtered) > 1 - return "ambiguous match" - endif - - return l:filtered[0].info -endfunction - -function! s:info_complete(echo, result) abort - if a:echo - call go#util#ShowInfo(a:result) - endif - - return a:result -endfunction - -function! s:trim_bracket(val) abort - let a:val.word = substitute(a:val.word, '[(){}\[\]]\+$', '', '') - return a:val -endfunction - -let s:completions = [] - -function! go#complete#GocodeComplete(findstart, base) abort - "findstart = 1 when we need to get the text length - if a:findstart == 1 - let l:completions = [] - execute "silent let l:completions = " . s:gocodeAutocomplete() - - if len(l:completions) == 0 || len(l:completions) >= 2 && len(l:completions[1]) == 0 - " no matches. cancel and leave completion mode. - call go#util#EchoInfo("no matches") - return -3 - endif - - let s:completions = l:completions[1] - return col('.') - l:completions[0] - 1 - "findstart = 0 when we need to return the list of completions - else - let s = getline(".")[col('.') - 1] - if s =~ '[(){}\{\}]' - return map(copy(s:completions[1]), 's:trim_bracket(v:val)') - endif - - return s:completions[1] - endif + return go#lsp#GetInfo() endfunction function! go#complete#Complete(findstart, base) abort - let l:state = {'done': 0, 'matches': []} + if !go#config#GoplsEnabled() + return -3 + endif - function! s:handler(state, matches) abort dict + let l:state = {'done': 0, 'matches': [], 'start': -1} + + function! s:handler(state, start, matches) abort dict + let a:state.start = a:start let a:state.matches = a:matches let a:state.done = 1 endfunction "findstart = 1 when we need to get the start of the match if a:findstart == 1 - call go#lsp#Completion(expand('%:p'), line('.'), col('.'), funcref('s:handler', [l:state])) + let [l:line, l:col] = getpos('.')[1:2] + let [l:line, l:col] = go#lsp#lsp#Position(l:line, l:col) + let l:completion = go#lsp#Completion(expand('%:p'), l:line, l:col, funcref('s:handler', [l:state])) + if l:completion + return -3 + endif while !l:state.done sleep 10m endwhile - let s:completions = l:state.matches - if len(l:state.matches) == 0 " no matches. cancel and leave completion mode. call go#util#EchoInfo("no matches") return -3 endif - return col('.') + let s:completions = l:state.matches + + return go#lsp#lsp#PositionOf(getline(l:line+1), l:state.start-1) + else "findstart = 0 when we need to return the list of completions return s:completions endif @@ -281,11 +53,11 @@ function! go#complete#ToggleAutoTypeInfo() abort if go#config#AutoTypeInfo() call go#config#SetAutoTypeInfo(0) call go#util#EchoProgress("auto type info disabled") - return - end - - call go#config#SetAutoTypeInfo(1) - call go#util#EchoProgress("auto type info enabled") + else + call go#config#SetAutoTypeInfo(1) + call go#util#EchoProgress("auto type info enabled") + endif + call go#auto#update_autocmd() endfunction " restore Vi compatibility settings diff --git a/pack/acp/start/vim-go/autoload/go/complete_test.vim b/pack/acp/start/vim-go/autoload/go/complete_test.vim index 4397142..ee00056 100644 --- a/pack/acp/start/vim-go/autoload/go/complete_test.vim +++ b/pack/acp/start/vim-go/autoload/go/complete_test.vim @@ -2,23 +2,24 @@ let s:cpo_save = &cpo set cpo&vim -func! Test_GetInfo() +func! Test_GetInfo_gopls() + let g:go_info_mode = 'gopls' + call s:getinfo() + unlet g:go_info_mode +endfunction + +func! s:getinfo() let l:filename = 'complete/complete.go' let l:tmp = gotest#load_fixture(l:filename) + try + call cursor(8, 3) - call cursor(8, 3) - - let g:go_info_mode = 'gocode' - let expected = 'func Example(s string)' - let actual = go#complete#GetInfo() - call assert_equal(expected, actual) - - let g:go_info_mode = 'guru' - call go#config#InfoMode() - let actual = go#complete#GetInfo() - call assert_equal(expected, actual) - - unlet g:go_info_mode + let expected = 'func Example(s string)' + let actual = go#complete#GetInfo() + call assert_equal(expected, actual) + finally + call delete(l:tmp, 'rf') + endtry endfunction " restore Vi compatibility settings diff --git a/pack/acp/start/vim-go/autoload/go/config.vim b/pack/acp/start/vim-go/autoload/go/config.vim index a3ef0a6..0e4fdeb 100644 --- a/pack/acp/start/vim-go/autoload/go/config.vim +++ b/pack/acp/start/vim-go/autoload/go/config.vim @@ -21,10 +21,12 @@ endfunction function! go#config#SetBuildTags(value) abort if a:value is '' silent! unlet g:go_build_tags + call go#lsp#ResetWorkspaceDirectories() return endif let g:go_build_tags = a:value + call go#lsp#ResetWorkspaceDirectories() endfunction function! go#config#TestTimeout() abort @@ -47,8 +49,23 @@ function! go#config#TermMode() abort return get(g:, 'go_term_mode', 'vsplit') endfunction +function! go#config#TermCloseOnExit() abort + return get(g:, 'go_term_close_on_exit', 1) +endfunction + +function! go#config#TermReuse() abort + return get(g:, 'go_term_reuse', 0) +endfunction + +function! go#config#SetTermCloseOnExit(value) abort + let g:go_term_close_on_exit = a:value +endfunction + function! go#config#TermEnabled() abort - return has('nvim') && get(g:, 'go_term_enabled', 0) + " nvim always support + " vim will support if terminal feature exists + let l:support = has('nvim') || has('terminal') + return support && get(g:, 'go_term_enabled', 0) endfunction function! go#config#SetTermEnabled(value) abort @@ -72,7 +89,18 @@ function! go#config#StatuslineDuration() abort endfunction function! go#config#SnippetEngine() abort - return get(g:, 'go_snippet_engine', 'automatic') + let l:engine = get(g:, 'go_snippet_engine', 'automatic') + if l:engine is? "automatic" + if get(g:, 'did_plugin_ultisnips') is 1 + let l:engine = 'ultisnips' + elseif get(g:, 'loaded_neosnippet') is 1 + let l:engine = 'neosnippet' + elseif get(g:, 'loaded_minisnip') is 1 + let l:engine = 'minisnip' + endif + endif + + return l:engine endfunction function! go#config#PlayBrowserCommand() abort @@ -114,7 +142,7 @@ function! go#config#ListAutoclose() abort endfunction function! go#config#InfoMode() abort - return get(g:, 'go_info_mode', 'gocode') + return get(g:, 'go_info_mode', 'gopls') endfunction function! go#config#GuruScope() abort @@ -139,30 +167,13 @@ function! go#config#SetGuruScope(scope) abort endif endfunction -function! go#config#GocodeUnimportedPackages() abort - return get(g:, 'go_gocode_unimported_packages', 0) -endfunction - -let s:sock_type = (has('win32') || has('win64')) ? 'tcp' : 'unix' -function! go#config#GocodeSocketType() abort - return get(g:, 'go_gocode_socket_type', s:sock_type) -endfunction - -function! go#config#GocodeProposeBuiltins() abort - return get(g:, 'go_gocode_propose_builtins', 1) -endfunction - -function! go#config#GocodeProposeSource() abort - return get(g:, 'go_gocode_propose_source', 0) -endfunction - function! go#config#EchoCommandInfo() abort return get(g:, 'go_echo_command_info', 1) endfunction function! go#config#DocUrl() abort - let godoc_url = get(g:, 'go_doc_url', 'https://godoc.org') - if godoc_url isnot 'https://godoc.org' + let godoc_url = get(g:, 'go_doc_url', 'https://pkg.go.dev') + if godoc_url isnot 'https://pkg.go.dev' " strip last '/' character if available let last_char = strlen(godoc_url) - 1 if godoc_url[last_char] == '/' @@ -174,12 +185,15 @@ function! go#config#DocUrl() abort return godoc_url endfunction +function! go#config#DocPopupWindow() abort + return get(g:, 'go_doc_popup_window', 0) +endfunction function! go#config#DefReuseBuffer() abort return get(g:, 'go_def_reuse_buffer', 0) endfunction function! go#config#DefMode() abort - return get(g:, 'go_def_mode', 'guru') + return get(g:, 'go_def_mode', 'gopls') endfunction function! go#config#DeclsIncludes() abort @@ -192,9 +206,10 @@ endfunction function! go#config#DebugWindows() abort return get(g:, 'go_debug_windows', { - \ 'stack': 'leftabove 20vnew', - \ 'out': 'botright 10new', \ 'vars': 'leftabove 30vnew', + \ 'stack': 'leftabove 20new', + \ 'goroutines': 'botright 10new', + \ 'out': 'botright 5new', \ } \ ) @@ -211,7 +226,7 @@ function! go#config#DebugCommands() abort endfunction function! go#config#DebugLogOutput() abort - return get(g:, 'go_debug_log_output', 'debugger, rpc') + return get(g:, 'go_debug_log_output', 'debugger,rpc') endfunction function! go#config#LspLog() abort @@ -236,6 +251,10 @@ function! go#config#AddtagsTransform() abort return get(g:, 'go_addtags_transform', "snakecase") endfunction +function! go#config#AddtagsSkipUnexported() abort + return get(g:, 'go_addtags_skip_unexported', 0) +endfunction + function! go#config#TemplateAutocreate() abort return get(g:, "go_template_autocreate", 1) endfunction @@ -245,31 +264,15 @@ function! go#config#SetTemplateAutocreate(value) abort endfunction function! go#config#MetalinterCommand() abort - return get(g:, "go_metalinter_command", "gometalinter") + return get(g:, "go_metalinter_command", "golangci-lint") endfunction function! go#config#MetalinterAutosaveEnabled() abort - let l:default_enabled = ["vet", "golint"] - - if go#config#MetalinterCommand() == "golangci-lint" - let l:default_enabled = ["govet", "golint"] - endif - - return get(g:, "go_metalinter_autosave_enabled", default_enabled) + return get(g:, "go_metalinter_autosave_enabled", ["govet", "golint"]) endfunction function! go#config#MetalinterEnabled() abort - let l:default_enabled = ["vet", "golint", "errcheck"] - - if go#config#MetalinterCommand() == "golangci-lint" - let l:default_enabled = ["govet", "golint"] - endif - - return get(g:, "go_metalinter_enabled", default_enabled) -endfunction - -function! go#config#MetalinterDisabled() abort - return get(g:, "go_metalinter_disabled", []) + return get(g:, "go_metalinter_enabled", ["vet", "golint", "errcheck"]) endfunction function! go#config#GolintBin() abort @@ -296,6 +299,10 @@ function! go#config#FmtAutosave() abort return get(g:, "go_fmt_autosave", 1) endfunction +function! go#config#ImportsAutosave() abort + return get(g:, 'go_imports_autosave', 0) +endfunction + function! go#config#SetFmtAutosave(value) abort let g:go_fmt_autosave = a:value endfunction @@ -340,8 +347,12 @@ function! go#config#FmtCommand() abort return get(g:, "go_fmt_command", "gofmt") endfunction +function! go#config#ImportsMode() abort + return get(g:, "go_imports_mode", "goimports") +endfunction + function! go#config#FmtOptions() abort - return get(g:, "go_fmt_options", {}) + return get(b:, "go_fmt_options", get(g:, "go_fmt_options", {})) endfunction function! go#config#FmtFailSilently() abort @@ -356,8 +367,13 @@ function! go#config#PlayOpenBrowser() abort return get(g:, "go_play_open_browser", 1) endfunction +function! go#config#RenameCommand() abort + " delegate to go#config#GorenameBin for backwards compatability. + return get(g:, "go_rename_command", go#config#GorenameBin()) +endfunction + function! go#config#GorenameBin() abort - return get(g:, "go_gorename_bin", "gorename") + return get(g:, "go_gorename_bin", "gopls") endfunction function! go#config#GorenamePrefill() abort @@ -451,10 +467,22 @@ function! go#config#HighlightVariableDeclarations() abort return get(g:, 'go_highlight_variable_declarations', 0) endfunction +function! go#config#HighlightDiagnosticErrors() abort + return get(g:, 'go_highlight_diagnostic_errors', 1) +endfunction + +function! go#config#HighlightDiagnosticWarnings() abort + return get(g:, 'go_highlight_diagnostic_warnings', 1) +endfunction + function! go#config#HighlightDebug() abort return get(g:, 'go_highlight_debug', 1) endfunction +function! go#config#DebugBreakpointSignText() abort + return get(g:, 'go_debug_breakpoint_sign_text', '>') +endfunction + function! go#config#FoldEnable(...) abort if a:0 > 0 return index(go#config#FoldEnable(), a:1) > -1 @@ -466,6 +494,82 @@ function! go#config#EchoGoInfo() abort return get(g:, "go_echo_go_info", 1) endfunction +function! go#config#CodeCompletionEnabled() abort + return get(g:, "go_code_completion_enabled", 1) +endfunction + +function! go#config#CodeCompletionIcase() abort + return get(g:, "go_code_completion_icase", 0) +endfunction + +function! go#config#Updatetime() abort + let go_updatetime = get(g:, 'go_updatetime', 800) + return go_updatetime == 0 ? &updatetime : go_updatetime +endfunction + +function! go#config#ReferrersMode() abort + return get(g:, 'go_referrers_mode', 'gopls') +endfunction + +function! go#config#ImplementsMode() abort + return get(g:, 'go_implements_mode', 'guru') +endfunction + +function! go#config#GoplsCompleteUnimported() abort + return get(g:, 'go_gopls_complete_unimported', v:null) +endfunction + +function! go#config#GoplsDeepCompletion() abort + return get(g:, 'go_gopls_deep_completion', v:null) +endfunction + +function! go#config#GoplsMatcher() abort + if !exists('g:go_gopls_matcher') && get(g:, 'g:go_gopls_fuzzy_matching', v:null) is 1 + return 'fuzzy' + endif + return get(g:, 'go_gopls_matcher', v:null) +endfunction + +function! go#config#GoplsStaticCheck() abort + return get(g:, 'go_gopls_staticcheck', v:null) +endfunction + +function! go#config#GoplsUsePlaceholders() abort + return get(g:, 'go_gopls_use_placeholders', v:null) +endfunction + +function! go#config#GoplsTempModfile() abort + return get(g:, 'go_gopls_temp_modfile', v:null) +endfunction + +function! go#config#GoplsAnalyses() abort + return get(g:, 'go_gopls_analyses', v:null) +endfunction + +function! go#config#GoplsLocal() abort + return get(g:, 'go_gopls_local', v:null) +endfunction + +function! go#config#GoplsGofumpt() abort + return get(g:, 'go_gopls_gofumpt', v:null) +endfunction + +function! go#config#GoplsSettings() abort + return get(g:, 'go_gopls_settings', v:null) +endfunction + +function! go#config#GoplsEnabled() abort + return get(g:, 'go_gopls_enabled', 1) +endfunction + +function! go#config#DiagnosticsEnabled() abort + return get(g:, 'go_diagnostics_enabled', 0) +endfunction + +function! go#config#GoplsOptions() abort + return get(g:, 'go_gopls_options', ['-remote=auto']) +endfunction + " Set the default value. A value of "1" is a shortcut for this, for " compatibility reasons. if exists("g:go_gorename_prefill") && g:go_gorename_prefill == 1 diff --git a/pack/acp/start/vim-go/autoload/go/config_test.vim b/pack/acp/start/vim-go/autoload/go/config_test.vim new file mode 100644 index 0000000..6ac3eed --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/config_test.vim @@ -0,0 +1,107 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +scriptencoding utf-8 + +func! Test_SetBuildTags() abort + if !go#util#has_job() + return + endif + + try + let g:go_def_mode = 'gopls' + let l:dir = 'test-fixtures/config/buildtags' + let l:jumpstart = [0, 4, 2, 0] + + execute 'e ' . printf('%s/buildtags.go', l:dir) + let l:jumpstartbuf = bufnr('') + + call setpos('.', [l:jumpstartbuf, l:jumpstart[1], l:jumpstart[2], 0]) + + let l:expectedfilename = printf('%s/foo.go', l:dir) + + let l:expected = [0, 5, 1, 0] + call assert_notequal(l:expected, l:jumpstart) + + call go#def#Jump('', 0) + + let l:start = reltime() + while getpos('.') != l:expected && reltimefloat(reltime(l:start)) < 10 + sleep 100m + endwhile + + call assert_equal(l:expectedfilename, bufname("%")) + call assert_equal(l:expected, getpos('.')) + + execute 'e ' . printf('%s/buildtags.go', l:dir) + + " prepare to wait for the workspace/configuration request + let g:go_debug=['lsp'] + + " set the build constraint + call go#config#SetBuildTags('constrained') + + " wait for the workspace/configuration request + let l:lsplog = getbufline('__GOLSP_LOG__', 1, '$') + let l:start = reltime() + while match(l:lsplog, 'workspace/configuration') == -1 && reltimefloat(reltime(l:start)) < 10 + sleep 50m + let l:lsplog = getbufline('__GOLSP_LOG__', 1, '$') + endwhile + unlet g:go_debug + " close the __GOLSP_LOG__ window + only + + " verify the cursor position within buildtags.go + call setpos('.', [l:jumpstartbuf, l:jumpstart[1], l:jumpstart[2], 0]) + call assert_equal(l:jumpstart, getpos('.')) + + let l:expectedfilename = printf('%s/constrainedfoo.go', l:dir) + let l:expected = [0, 6, 1, 0] + call assert_notequal(l:expected, l:jumpstart) + + call go#def#Jump('', 0) + + let l:start = reltime() + while getpos('.') != l:expected && reltimefloat(reltime(l:start)) < 10 + sleep 100m + endwhile + + call assert_equal(l:expectedfilename, bufname("%")) + call assert_equal(l:expected, getpos('.')) + + let l:lsplog = getbufline('__GOLSP_LOG__', 1, '$') + + finally + call go#config#SetBuildTags('') + unlet g:go_def_mode + endtry +endfunc + +func! Test_GoplsEnabled_Clear() abort + if !go#util#has_job() + return + endif + + try + let g:go_gopls_enabled = 0 + + let l:tmp = gotest#write_file('gopls_disabled.go', [ + \ 'package example', + \ '', + \ 'func Example() {', + \ "\tprintln(" . '"hello, world!")', + \ '}', + \ ] ) + + finally + unlet g:go_gopls_enabled + call delete(l:tmp, 'rf') + endtry +endfunc +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/coverage.vim b/pack/acp/start/vim-go/autoload/go/coverage.vim index 50d4df1..01456e5 100644 --- a/pack/acp/start/vim-go/autoload/go/coverage.vim +++ b/pack/acp/start/vim-go/autoload/go/coverage.vim @@ -25,10 +25,10 @@ endfunction " the code. Calling it again reruns the tests and shows the last updated " coverage. function! go#coverage#Buffer(bang, ...) abort - " we use matchaddpos() which was introduce with 7.4.330, be sure we have - " it: http://ftp.vim.org/vim/patches/7.4/7.4.330 + + " check if the version of Vim being tested supports matchaddpos() if !exists("*matchaddpos") - call go#util#EchoError("GoCoverage is supported with Vim version 7.4-330 or later") + call go#util#EchoError("GoCoverage is not supported by your version of Vim.") return -1 endif diff --git a/pack/acp/start/vim-go/autoload/go/debug.vim b/pack/acp/start/vim-go/autoload/go/debug.vim index 1d11301..abe7144 100644 --- a/pack/acp/start/vim-go/autoload/go/debug.vim +++ b/pack/acp/start/vim-go/autoload/go/debug.vim @@ -6,14 +6,15 @@ scriptencoding utf-8 if !exists('s:state') let s:state = { - \ 'rpcid': 1, - \ 'running': 0, - \ 'currentThread': {}, - \ 'localVars': {}, - \ 'functionArgs': {}, - \ 'message': [], - \ 'is_test': 0, - \} + \ 'rpcid': 1, + \ 'running': 0, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \ 'resultHandlers': {}, + \ 'kill_on_detach': v:true, + \ } if go#util#HasDebug('debugger-state') call go#config#SetDebugDiag(s:state) @@ -24,7 +25,7 @@ if !exists('s:start_args') let s:start_args = [] endif -function! s:groutineID() abort +function! s:goroutineID() abort return s:state['currentThread'].goroutineID endfunction @@ -81,15 +82,23 @@ function! s:logger(prefix, ch, msg) abort endtry endfunction -function! s:call_jsonrpc(method, ...) abort +" s:call_jsonrpc will call method, passing all of s:call_jsonrpc's optional +" arguments in the rpc request's params field. + +" The first argument to s:call_jsonrpc should be a function that takes two +" arguments. The first argument will be a function that takes no arguments and will +" throw an exception if the response to the request is an error response. The +" second argument is the response itself. +function! s:call_jsonrpc(handle_result, method, ...) abort if go#util#HasDebug('debugger-commands') - echom 'sending to dlv ' . a:method + call go#util#EchoInfo('sending to dlv ' . a:method) endif let l:args = a:000 let s:state['rpcid'] += 1 + let l:reqid = s:state['rpcid'] let l:req_json = json_encode({ - \ 'id': s:state['rpcid'], + \ 'id': l:reqid, \ 'method': a:method, \ 'params': l:args, \}) @@ -98,36 +107,33 @@ function! s:call_jsonrpc(method, ...) abort let l:ch = s:state['ch'] if has('nvim') call chansend(l:ch, l:req_json) - while len(s:state.data) == 0 - sleep 50m - if get(s:state, 'ready', 0) == 0 - return - endif - endwhile - let resp_json = s:state.data[0] - let s:state.data = s:state.data[1:] else call ch_sendraw(l:ch, req_json) - let l:resp_raw = ch_readraw(l:ch) - let resp_json = json_decode(l:resp_raw) endif + let s:state.resultHandlers[l:reqid] = a:handle_result + if go#util#HasDebug('debugger-commands') let g:go_debug_commands = add(go#config#DebugCommands(), { \ 'request': l:req_json, - \ 'response': l:resp_json, \ }) endif - if type(l:resp_json) == v:t_dict && has_key(l:resp_json, 'error') && !empty(l:resp_json.error) - throw l:resp_json.error - endif - return l:resp_json + redraw catch throw substitute(v:exception, '^Vim', '', '') endtry endfunction +function! s:exited(res) abort + if type(a:res) ==# type(v:null) + return 0 + endif + + let state = a:res.result.State + return state.exited == v:true +endfunction + " Update the location of the current breakpoint or line we're halted on based on " response from dlv. function! s:update_breakpoint(res) abort @@ -155,13 +161,23 @@ function! s:update_breakpoint(res) abort endif silent! exe 'norm!' linenr.'G' silent! normal! zvzz + " TODO(bc): convert to use s:sign_unplace() silent! sign unplace 9999 + " TODO(bc): convert to use s:sign_place() silent! exe 'sign place 9999 line=' . linenr . ' name=godebugcurline file=' . filename + call s:warn_when_stale(fnamemodify(l:filename, ':p')) endfunction " Populate the stacktrace window. -function! s:show_stacktrace(res) abort - if !has_key(a:res, 'result') +function! s:show_stacktrace(check_errors, res) abort + try + call a:check_errors() + catch + call go#util#EchoError(printf('could not update stack: %s', v:exception)) + return + endtry + + if type(a:res) isnot type({}) || !has_key(a:res, 'result') || empty(a:res.result) return endif @@ -178,6 +194,9 @@ function! s:show_stacktrace(res) abort silent %delete _ for i in range(len(a:res.result.Locations)) let loc = a:res.result.Locations[i] + if loc.file is# '?' || !has_key(loc, 'function') + continue + endif call setline(i+1, printf('%s - %s:%d', loc.function.name, fnamemodify(loc.file, ':p'), loc.line)) endfor finally @@ -224,6 +243,7 @@ function! s:show_variables() abort endfunction function! s:clearState() abort + let s:state['running'] = 0 let s:state['currentThread'] = {} let s:state['localVars'] = {} let s:state['functionArgs'] = {} @@ -233,7 +253,7 @@ function! s:clearState() abort endfunction function! s:stop() abort - let l:res = s:call_jsonrpc('RPCServer.Detach', {'kill': v:true}) + call s:call_jsonrpc(function('s:noop'), 'RPCServer.Detach', {'kill': s:state['kill_on_detach']}) if has_key(s:state, 'job') call go#job#Wait(s:state['job']) @@ -260,8 +280,9 @@ function! go#debug#Stop() abort for k in map(split(execute('command GoDebug'), "\n")[1:], 'matchstr(v:val, "^\\s*\\zs\\S\\+")') exe 'delcommand' k endfor - command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start(0, ) - command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start(1, ) + command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start('debug', ) + command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start('test', ) + command! -nargs=1 GoDebugAttach call go#debug#Start('attach', ) command! -nargs=? GoDebugBreakpoint call go#debug#Breakpoint() " Remove all mappings. @@ -280,6 +301,7 @@ function! go#debug#Stop() abort silent! exe bufwinnr(bufnr('__GODEBUG_STACKTRACE__')) 'wincmd c' silent! exe bufwinnr(bufnr('__GODEBUG_VARIABLES__')) 'wincmd c' silent! exe bufwinnr(bufnr('__GODEBUG_OUTPUT__')) 'wincmd c' + silent! exe bufwinnr(bufnr('__GODEBUG_GOROUTINES__')) 'wincmd c' if has('balloon_eval') let &ballooneval=s:ballooneval @@ -326,10 +348,10 @@ endfunction function! s:expand_var() abort " Get name from struct line. - let name = matchstr(getline('.'), '^[^:]\+\ze: [a-zA-Z0-9\.·]\+{\.\.\.}$') + let name = matchstr(getline('.'), '^[^:]\+\ze: \*\?[a-zA-Z0-9-_/\.]\+\({\.\.\.}\)\?$') " Anonymous struct if name == '' - let name = matchstr(getline('.'), '^[^:]\+\ze: struct {.\{-}}$') + let name = matchstr(getline('.'), '^[^:]\+\ze: \*\?struct {.\{-}}$') endif if name != '' @@ -419,23 +441,6 @@ function! s:start_cb() abort endif let debugwindows = go#config#DebugWindows() - if has_key(debugwindows, "stack") && debugwindows['stack'] != '' - exe 'silent ' . debugwindows['stack'] - silent file `='__GODEBUG_STACKTRACE__'` - setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline - setlocal filetype=godebugstacktrace - nmap :call goto_file() - nmap q (go-debug-stop) - endif - - if has_key(debugwindows, "out") && debugwindows['out'] != '' - exe 'silent ' . debugwindows['out'] - silent file `='__GODEBUG_OUTPUT__'` - setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline - setlocal filetype=godebugoutput - nmap q (go-debug-stop) - endif - if has_key(debugwindows, "vars") && debugwindows['vars'] != '' exe 'silent ' . debugwindows['vars'] silent file `='__GODEBUG_VARIABLES__'` @@ -446,24 +451,66 @@ function! s:start_cb() abort nmap q (go-debug-stop) endif + if has_key(debugwindows, "stack") && debugwindows['stack'] != '' + exe 'silent ' . debugwindows['stack'] + silent file `='__GODEBUG_STACKTRACE__'` + setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline + setlocal filetype=godebugstacktrace + nmap :call goto_file() + nmap q (go-debug-stop) + endif + + if has_key(debugwindows, "goroutines") && debugwindows['goroutines'] != '' + exe 'silent ' . debugwindows['goroutines'] + silent file `='__GODEBUG_GOROUTINES__'` + setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline + setlocal filetype=godebugvariables + call append(0, ["# Goroutines"]) + nmap :call go#debug#Goroutine() + endif + + if has_key(debugwindows, "out") && debugwindows['out'] != '' + exe 'silent ' . debugwindows['out'] + silent file `='__GODEBUG_OUTPUT__'` + setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline + setlocal filetype=godebugoutput + nmap q (go-debug-stop) + endif + call win_gotoid(l:winid) + silent! delcommand GoDebugStart silent! delcommand GoDebugTest + silent! delcommand GoDebugAttach + command! -nargs=0 GoDebugContinue call go#debug#Stack('continue') + command! -nargs=0 GoDebugStop call go#debug#Stop() + + nnoremap (go-debug-breakpoint) :call go#debug#Breakpoint() + nnoremap (go-debug-continue) :call go#debug#Stack('continue') + nnoremap (go-debug-stop) :call go#debug#Stop() + + augroup vim-go-debug + autocmd! * + autocmd FileType go nmap (go-debug-continue) + autocmd FileType go nmap (go-debug-breakpoint) + augroup END + doautocmd vim-go-debug FileType go +endfunction + +function! s:continue() command! -nargs=0 GoDebugNext call go#debug#Stack('next') command! -nargs=0 GoDebugStep call go#debug#Stack('step') command! -nargs=0 GoDebugStepOut call go#debug#Stack('stepOut') command! -nargs=0 GoDebugRestart call go#debug#Restart() - command! -nargs=0 GoDebugStop call go#debug#Stop() command! -nargs=* GoDebugSet call go#debug#Set() command! -nargs=1 GoDebugPrint call go#debug#Print() + command! -nargs=0 GoDebugHalt call go#debug#Stack('halt') - nnoremap (go-debug-breakpoint) :call go#debug#Breakpoint() nnoremap (go-debug-next) :call go#debug#Stack('next') nnoremap (go-debug-step) :call go#debug#Stack('step') - nnoremap (go-debug-stepout) :call go#debug#Stack('stepout') - nnoremap (go-debug-continue) :call go#debug#Stack('continue') - nnoremap (go-debug-stop) :call go#debug#Stop() + nnoremap (go-debug-stepout) :call go#debug#Stack('stepOut') nnoremap (go-debug-print) :call go#debug#Print(expand('')) + nnoremap (go-debug-halt) :call go#debug#Stack('halt') if has('balloon_eval') let s:balloonexpr=&balloonexpr @@ -473,8 +520,6 @@ function! s:start_cb() abort set ballooneval endif - call win_gotoid(l:winid) - augroup vim-go-debug autocmd! * autocmd FileType go nmap (go-debug-continue) @@ -482,13 +527,14 @@ function! s:start_cb() abort autocmd FileType go nmap (go-debug-breakpoint) autocmd FileType go nmap (go-debug-next) autocmd FileType go nmap (go-debug-step) + autocmd FileType go nmap (go-debug-halt) augroup END doautocmd vim-go-debug FileType go endfunction function! s:err_cb(ch, msg) abort if get(s:state, 'ready', 0) != 0 - call call('s:logger', ['ERR: ', a:ch, a:msg]) + call s:logger('ERR: ', a:ch, a:msg) return endif @@ -497,20 +543,21 @@ endfunction function! s:out_cb(ch, msg) abort if get(s:state, 'ready', 0) != 0 - call call('s:logger', ['OUT: ', a:ch, a:msg]) + call s:logger('OUT: ', a:ch, a:msg) return endif let s:state['message'] += [a:msg] if stridx(a:msg, go#config#DebugAddress()) != -1 + let s:state['data'] = [] + let l:state = {'databuf': ''} + + " explicitly bind callback to state so that within it, self will + " always refer to state. See :help Partial for more information. + let l:state.on_data = function('s:on_data', [], l:state) + if has('nvim') - let s:state['data'] = [] - let l:state = {'databuf': ''} - - " explicitly bind callback to state so that within it, self will - " always refer to state. See :help Partial for more information. - let l:state.on_data = function('s:on_data', [], l:state) let l:ch = sockconnect('tcp', go#config#DebugAddress(), {'on_data': l:state.on_data, 'state': l:state}) if l:ch == 0 call go#util#EchoError("could not connect to debugger") @@ -518,7 +565,7 @@ function! s:out_cb(ch, msg) abort return endif else - let l:ch = ch_open(go#config#DebugAddress(), {'mode': 'raw', 'timeout': 20000}) + let l:ch = ch_open(go#config#DebugAddress(), {'mode': 'raw', 'timeout': 20000, 'callback': l:state.on_data}) if ch_status(l:ch) !=# 'open' call go#util#EchoError("could not connect to debugger") call go#job#Stop(s:state['job']) @@ -534,9 +581,8 @@ function! s:out_cb(ch, msg) abort let s:state['ready'] = 1 " replace all the breakpoints set before delve started so that the ids won't overlap. - let l:breakpoints = s:list_breakpoints() for l:bt in s:list_breakpoints() - exe 'sign unplace '. l:bt.id + call s:sign_unplace(l:bt.id, l:bt.file) call go#debug#Breakpoint(l:bt.line, l:bt.file) endfor @@ -544,27 +590,108 @@ function! s:out_cb(ch, msg) abort endif endfunction -function! s:on_data(ch, data, event) dict abort - let l:data = self.databuf - for l:msg in a:data - let l:data .= l:msg - endfor +" s:on_data's third optional argument is provided, but not used, so that the +" same function can be used for Vim's 'callback' and Neovim's 'data'. +function! s:on_data(ch, data, ...) dict abort + let l:data = s:message(self.databuf, a:data) + let l:messages = split(l:data, "\n") + for l:msg in l:messages + let l:data = l:messages[0] + try + let l:res = json_decode(l:data) + " remove the decoded message + call remove(l:messages, 0) + catch + return + finally + " Rejoin messages and assign to databuf so that any messages that come + " in if s:handleRPCResult sleeps will be appended correctly. + " + " Because the current message is removed in the try immediately after + " decoding, that l:messages contains all the messages that have not + " yet been decoded including the current message if decoding it + " failed. + let self.databuf = join(l:messages, "\n") + endtry + + if go#util#HasDebug('debugger-commands') + let g:go_debug_commands = add(go#config#DebugCommands(), { + \ 'response': l:data, + \ }) + endif + call s:handleRPCResult(l:res) + endfor +endfunction + +function! s:message(buf, data) abort + if has('nvim') + " dealing with the channel lines of Neovim is awful. The docs (:help + " channel-lines) say: + " stream event handlers may receive partial (incomplete) lines. For a + " given invocation of on_stdout etc, `a:data` is not guaranteed to end + " with a newline. + " - `abcdefg` may arrive as `['abc']`, `['defg']`. + " - `abc\nefg` may arrive as `['abc', '']`, `['efg']` or `['abc']`, + " `['','efg']`, or even `['ab']`, `['c','efg']`. + " + " Thankfully, though, this is explained a bit better in an issue: + " https://github.com/neovim/neovim/issues/3555. Specifically in these two + " comments: + " * https://github.com/neovim/neovim/issues/3555#issuecomment-152290804 + " * https://github.com/neovim/neovim/issues/3555#issuecomment-152588749 + " + " The key is + " Every item in the list passed to job control callbacks represents a + " string after a newline(Except the first, of course). If the program + " outputs: "hello\nworld" the corresponding list is ["hello", "world"]. + " If the program outputs "hello\nworld\n", the corresponding list is + " ["hello", "world", ""]. In other words, you can always determine if + " the last line received is complete or not. + " and + " for every list you receive in a callback, all items except the first + " represent newlines. + + let l:data = printf('%s%s', a:buf, a:data[0]) + for l:msg in a:data[1:] + let l:data = printf("%s\n%s", l:data, l:msg) + endfor + + return l:data + endif + + return printf('%s%s', a:buf, a:data) +endfunction + +" s:check_errors will be curried and injected into rpc result handlers so that +" those result handlers can consistently check for errors in the response by +" catching exceptions and handling the error appropriately. +function! s:check_errors(resp_json) abort + if type(a:resp_json) == v:t_dict && has_key(a:resp_json, 'error') && !empty(a:resp_json.error) + throw a:resp_json.error + endif +endfunction + +function! s:handleRPCResult(resp) abort try - let l:res = json_decode(l:data) - let s:state['data'] = add(s:state['data'], l:res) - let self.databuf = '' + let l:id = a:resp.id + " call the result handler with its first argument set to a curried + " s:check_errors value so that the result handler can call s:check_errors + " without passing any arguments to check whether the response is an error + " response. + call call(s:state.resultHandlers[l:id], [function('s:check_errors', [a:resp]), a:resp]) catch - " there isn't a complete message in databuf: buffer l:data and try - " again when more data comes in. - let self.databuf = l:data + throw v:exception finally + if has_key(s:state.resultHandlers, l:id) + call remove(s:state.resultHandlers, l:id) + endif endtry endfunction " Start the debug mode. The first argument is the package name to compile and " debug, anything else will be passed to the running program. -function! go#debug#Start(is_test, ...) abort +function! go#debug#Start(mode, ...) abort call go#cmd#autowrite() if !go#util#has_job() @@ -577,7 +704,7 @@ function! go#debug#Start(is_test, ...) abort return s:state['job'] endif - let s:start_args = a:000 + let s:start_args = [a:mode] + a:000 if go#util#HasDebug('debugger-state') call go#config#SetDebugDiag(s:state) @@ -589,39 +716,21 @@ function! go#debug#Start(is_test, ...) abort endif try - if len(a:000) > 0 - let l:pkgname = a:1 - if l:pkgname[0] == '.' - let l:pkgname = go#package#FromPath(l:pkgname) - endif + + let l:cmd = [dlv, a:mode] + + let s:state['kill_on_detach'] = v:true + if a:mode is 'debug' || a:mode is 'test' + let l:cmd = extend(l:cmd, s:package(a:000)) + let l:cmd = extend(l:cmd, ['--output', tempname()]) + elseif a:mode is 'attach' + let l:cmd = add(l:cmd, a:1) + let s:state['kill_on_detach'] = v:false else - let l:pkgname = go#package#FromPath(getcwd()) + call go#util#EchoError('Unknown dlv command') endif - if l:pkgname is -1 - call go#util#EchoError('could not determine package name') - return - endif - - " cd in to test directory; this is also what running "go test" does. - if a:is_test - " TODO(bc): Either remove this if it's ok to do so or else record it and - " reset cwd after the job completes. - lcd %:p:h - endif - - let s:state.is_test = a:is_test - - let l:args = [] - if len(a:000) > 1 - let l:args = ['--'] + a:000[1:] - endif - - let l:cmd = [ - \ dlv, - \ (a:is_test ? 'test' : 'debug'), - \ l:pkgname, - \ '--output', tempname(), + let l:cmd += [ \ '--headless', \ '--api-version', '2', \ '--listen', go#config#DebugAddress(), @@ -635,7 +744,10 @@ function! go#debug#Start(is_test, ...) abort if buildtags isnot '' let l:cmd += ['--build-flags', '--tags=' . buildtags] endif - let l:cmd += l:args + + if len(a:000) > 1 + let l:cmd += ['--'] + a:000[1:] + endif let s:state['message'] = [] let l:opts = { @@ -650,13 +762,46 @@ function! go#debug#Start(is_test, ...) abort let s:state['job'] = go#job#Start(l:cmd, l:opts) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not start debugger: %s', v:exception)) endtry return s:state['job'] endfunction -" Translate a reflect kind constant to a human string. +" s:package returns the import path of package name of a :GoDebug(Start|Test) +" call as a list so that the package can be appended to a command list using +" extend(). args is expected to be a (potentially empty_ list. The first +" element in args (if there are any) is expected to be a package path. An +" emnpty list is returned when either args is an empty list or the import path +" cannot be determined. +function! s:package(args) + if len(a:args) == 0 + return [] + endif + + " append the package when it's given. + let l:pkgname = a:args[0] + if l:pkgname[0] == '.' + let l:pkgabspath = fnamemodify(l:pkgname, ':p') + + let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' + let l:dir = getcwd() + let l:dir = go#util#Chdir(expand('%:p:h')) + try + let l:pkgname = go#package#FromPath(l:pkgabspath) + if type(l:pkgname) == type(0) + call go#util#EchoError('could not determine package name') + return [] + endif + finally + call go#util#Chdir(l:dir) + endtry + endif + + return [l:pkgname] +endfunction + + " Translate a reflect kind constant to a human string. function! s:reflect_kind(k) " Kind constants from Go's reflect package. return [ @@ -746,18 +891,25 @@ endfunction function! s:eval(arg) abort try - let l:res = s:call_jsonrpc('RPCServer.State') - let l:res = s:call_jsonrpc('RPCServer.Eval', { + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.State') + let l:res = l:promise.await() + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.Eval', { \ 'expr': a:arg, \ 'scope': {'GoroutineID': l:res.result.State.currentThread.goroutineID} \ }) + + let l:res = l:promise.await() + return s:eval_tree(l:res.result.Variable, 0) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('evaluation failed: %s', v:exception)) return '' endtry endfunction + function! go#debug#BalloonExpr() abort silent! let l:v = s:eval(v:beval_text) return l:v @@ -767,7 +919,106 @@ function! go#debug#Print(arg) abort try echo substitute(s:eval(a:arg), "\n$", "", 0) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not print: %s', v:exception)) + endtry +endfunction + +function! s:update_goroutines() abort + call s:call_jsonrpc(function('s:update_goroutines_state_handler'), 'RPCServer.State') +endfunction + +function! s:update_goroutines_state_handler(check_errors, res) abort + try + call a:check_errors() + + let l:currentGoroutineID = 0 + try + if type(a:res) is type({}) && has_key(a:res, 'result') && !empty(a:res['result']) + let l:currentGoroutineID = a:res["result"]["State"]["currentGoroutine"]["id"] + endif + catch + call go#util#EchoWarning("current goroutine not found...") + endtry + + call s:call_jsonrpc(function('s:list_goroutines_handler', [l:currentGoroutineID]), 'RPCServer.ListGoroutines') + catch + call go#util#EchoError(printf('could not list goroutines: %s', v:exception)) + endtry +endfunction + +function s:list_goroutines_handler(currentGoroutineID, check_errors, res) abort + try + call a:check_errors() + call s:show_goroutines(a:currentGoroutineID, a:res) + catch + call go#util#EchoError(printf('could not show goroutines: %s', v:exception)) + endtry +endfunction + +function! s:show_goroutines(currentGoroutineID, res) abort + let l:goroutines_winid = bufwinid('__GODEBUG_GOROUTINES__') + if l:goroutines_winid == -1 + return + endif + + let l:winid = win_getid() + call win_gotoid(l:goroutines_winid) + + try + setlocal modifiable + silent %delete _ + + let v = ['# Goroutines'] + + if type(a:res) isnot type({}) || !has_key(a:res, 'result') || empty(a:res['result']) + call setline(1, v) + return + endif + + let l:goroutines = a:res["result"]["Goroutines"] + if len(l:goroutines) == 0 + call go#util#EchoWarning("No Goroutines Running Now...") + call setline(1, v) + return + endif + + for l:idx in range(len(l:goroutines)) + let l:goroutine = l:goroutines[l:idx] + let l:goroutineType = "" + let l:loc = 0 + if l:goroutine.startLoc.file != "" + let l:loc = l:goroutine.startLoc + let l:goroutineType = "Start" + endif + if l:goroutine.goStatementLoc.file != "" + let l:loc = l:goroutine.goStatementLoc + let l:goroutineType = "Go" + endif + if l:goroutine.currentLoc.file != "" + let l:loc = l:goroutine.currentLoc + let l:goroutineType = "Runtime" + endif + if l:goroutine.userCurrentLoc.file != "" + let l:loc=l:goroutine.userCurrentLoc + let l:goroutineType = "User" + endif + + " The current goroutine can be changed by pressing enter on one of the + " lines listing a non-active goroutine. If the format of either of these + " lines is modified, then make sure that go#debug#Goroutine is also + " changed if needed. + if l:goroutine.id == a:currentGoroutineID + let l:g = printf("* Goroutine %s - %s: %s:%s %s (thread: %s)", l:goroutine.id, l:goroutineType, l:loc.file, l:loc.line, l:loc.function.name, l:goroutine.threadID) + else + let l:g = printf(" Goroutine %s - %s: %s:%s %s (thread: %s)", l:goroutine.id, l:goroutineType, l:loc.file, l:loc.line, l:loc.function.name, l:goroutine.threadID) + endif + let v += [l:g] + endfor + + call setline(1, v) + finally + setlocal nomodifiable + call win_gotoid(l:winid) endtry endfunction @@ -778,22 +1029,47 @@ function! s:update_variables() abort " MaxArrayValues is the maximum number of elements read from an array, a slice or a map. " MaxStructFields is the maximum number of fields read from a struct, -1 will read all fields. let l:cfg = { - \ 'scope': {'GoroutineID': s:groutineID()}, + \ 'scope': {'GoroutineID': s:goroutineID()}, \ 'cfg': {'MaxStringLen': 20, 'MaxArrayValues': 20} \ } try - let res = s:call_jsonrpc('RPCServer.ListLocalVars', l:cfg) - let s:state['localVars'] = res.result['Variables'] + call s:call_jsonrpc(function('s:handle_list_local_vars'), 'RPCServer.ListLocalVars', l:cfg) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not list variables: %s', v:exception)) endtry try - let res = s:call_jsonrpc('RPCServer.ListFunctionArgs', l:cfg) - let s:state['functionArgs'] = res.result['Args'] + call s:call_jsonrpc(function('s:handle_list_function_args'), 'RPCServer.ListFunctionArgs', l:cfg) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not list function arguments: %s', v:exception)) + endtry + +endfunction + +function! s:handle_list_local_vars(check_errors, res) abort + let s:state['localVars'] = {} + try + call a:check_errors() + if type(a:res) is type({}) && has_key(a:res, 'result') && !empty(a:res.result) + let s:state['localVars'] = a:res.result['Variables'] + endif + catch + call go#util#EchoWarning(printf('could not list variables: %s', v:exception)) + endtry + + call s:show_variables() +endfunction + +function! s:handle_list_function_args(check_errors, res) abort + let s:state['functionArgs'] = {} + try + call a:check_errors() + if type(a:res) is type({}) && has_key(a:res, 'result') && !empty(a:res.result) + let s:state['functionArgs'] = a:res.result['Args'] + endif + catch + call go#util#EchoWarning(printf('could not list function arguments: %s', v:exception)) endtry call s:show_variables() @@ -801,14 +1077,27 @@ endfunction function! go#debug#Set(symbol, value) abort try - let l:res = s:call_jsonrpc('RPCServer.State') - call s:call_jsonrpc('RPCServer.Set', { + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.State') + let l:res = l:promise.await() + + call s:call_jsonrpc(function('s:handle_set'), 'RPCServer.Set', { \ 'symbol': a:symbol, \ 'value': a:value, \ 'scope': {'GoroutineID': l:res.result.State.currentThread.goroutineID} \ }) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not set symbol value: %s', v:exception)) + endtry + + call s:update_variables() +endfunction + +function! s:handle_set(check_errors, res) abort + try + call a:check_errors() + catch + call go#util#EchoError(printf('could not set symbol value: %s', v:exception)) endtry call s:update_variables() @@ -816,20 +1105,25 @@ endfunction function! s:update_stacktrace() abort try - let l:res = s:call_jsonrpc('RPCServer.Stacktrace', {'id': s:groutineID(), 'depth': 5}) - call s:show_stacktrace(l:res) + call s:call_jsonrpc(function('s:show_stacktrace'), 'RPCServer.Stacktrace', {'id': s:goroutineID(), 'depth': 5}) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not update stack: %s', v:exception)) endtry endfunction function! s:stack_cb(res) abort let s:stack_name = '' - if empty(a:res) || !has_key(a:res, 'result') + if type(a:res) isnot type({}) || !has_key(a:res, 'result') || empty(a:res.result) + return + endif + + if s:exited(a:res) + call go#debug#Stop() return endif call s:update_breakpoint(a:res) + call s:update_goroutines() call s:update_stacktrace() call s:update_variables() endfunction @@ -844,9 +1138,11 @@ function! go#debug#Stack(name) abort if s:state.running is 0 let s:state.running = 1 let l:name = 'continue' + call s:continue() endif " Add a breakpoint to the main.Main if the user didn't define any. + " TODO(bc): actually set set the breakpoint in main.Main if len(s:list_breakpoints()) is 0 if go#debug#Breakpoint() isnot 0 let s:state.running = 0 @@ -855,21 +1151,67 @@ function! go#debug#Stack(name) abort endif try - " TODO: document why this is needed. + " s:stack_name is reset in s:stack_cb(). While its value is 'next', the + " current operation being performed by delve is a next operation and it + " must be cancelled before another next operation can start. See + " https://github.com/go-delve/delve/blob/ab5713d3ec5d12754f4b2edf85e4b36a08b67c48/Documentation/api/ClientHowto.md#special-continue-commands-and-asynchronous-breakpoints + " for more information. if l:name is# 'next' && get(s:, 'stack_name', '') is# 'next' - call s:call_jsonrpc('RPCServer.CancelNext') + " use s:rpc_response so that the any errors will be checked instead of + " completely discarding the result with s:noop. + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.CancelNext') + call l:promise.await() endif let s:stack_name = l:name try - let res = s:call_jsonrpc('RPCServer.Command', {'name': l:name}) - call s:stack_cb(res) + silent! sign unplace 9999 + call s:call_jsonrpc(function('s:handle_stack_response', [l:name]), 'RPCServer.Command', {'name': l:name}) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('rpc failure: %s', v:exception)) call s:clearState() + call go#util#EchoInfo('restarting debugger') call go#debug#Restart() endtry catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('CancelNext RPC call failed: %s', v:exception)) + endtry +endfunction + +function! s:handle_stack_response(command, check_errors, res) abort + try + call a:check_errors() + + if a:command is# 'next' + call s:handleNextInProgress(a:res) + endif + + call s:stack_cb(a:res) + catch + call go#util#EchoError(printf('rpc failure: %s', v:exception)) + call s:clearState() + call go#util#EchoInfo('restarting debugger') + call go#debug#Restart() + endtry +endfunction + +function! s:handleNextInProgress(res) + try + let l:res = a:res + let l:w = 0 + while l:w < 1 + if l:res.result.State.NextInProgress == v:true + " TODO(bc): message the user that a breakpoint was hit in a different + " goroutine while trying to resume. + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.Command', {'name': 'continue'}) + let l:res = l:promise.await() + else + return + endif + endwhile + catch + throw v:exception endtry endfunction @@ -880,17 +1222,19 @@ function! go#debug#Restart() abort call s:stop() let s:state = { - \ 'rpcid': 1, - \ 'running': 0, - \ 'currentThread': {}, - \ 'localVars': {}, - \ 'functionArgs': {}, - \ 'message': [], - \} + \ 'rpcid': 1, + \ 'running': 0, + \ 'currentThread': {}, + \ 'localVars': {}, + \ 'functionArgs': {}, + \ 'message': [], + \ 'resultHandlers': {}, + \ 'kill_on_detach': s:state['kill_on_detach'], + \ } call call('go#debug#Start', s:start_args) catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('restart failed: %s', v:exception)) endtry endfunction @@ -899,6 +1243,25 @@ function! s:isActive() return len(s:state['message']) > 0 endfunction +" Change Goroutine +function! go#debug#Goroutine() abort + let l:goroutineID = str2nr(substitute(getline('.'), '^ Goroutine \(.\{-1,\}\) - .*', '\1', 'g')) + + if l:goroutineID <= 0 + return + endif + + try + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.Command', {'Name': 'switchGoroutine', 'GoroutineID': l:goroutineID}) + let l:res = l:promise.await() + call s:stack_cb(l:res) + call go#util#EchoInfo("Switched goroutine to: " . l:goroutineID) + catch + call go#util#EchoError(printf('could not switch goroutine: %s', v:exception)) + endtry +endfunction + " Toggle breakpoint. Returns 0 on success and 1 on failure. function! go#debug#Breakpoint(...) abort let l:filename = fnamemodify(expand('%'), ':p:gs!\\!/!') @@ -928,64 +1291,181 @@ function! go#debug#Breakpoint(...) abort " Remove breakpoint. if type(l:found) == v:t_dict && !empty(l:found) - exe 'sign unplace '. l:found.id .' file=' . l:found.file + call s:sign_unplace(l:found.id, l:found.file) if s:isActive() - let res = s:call_jsonrpc('RPCServer.ClearBreakpoint', {'id': l:found.id}) + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.ClearBreakpoint', {'id': l:found.id}) + let res = l:promise.await() endif - " Add breakpoint. - else + else " Add breakpoint if s:isActive() - let l:res = s:call_jsonrpc('RPCServer.CreateBreakpoint', {'Breakpoint': {'file': l:filename, 'line': l:linenr}}) - let l:bt = res.result.Breakpoint - exe 'sign place '. l:bt.id .' line=' . l:bt.line . ' name=godebugbreakpoint file=' . l:bt.file + let l:promise = go#promise#New(function('s:rpc_response'), 20000, {}) + call s:call_jsonrpc(l:promise.wrapper, 'RPCServer.CreateBreakpoint', {'Breakpoint': {'file': l:filename, 'line': l:linenr}}) + let l:res = l:promise.await() + let l:bt = l:res.result.Breakpoint + call s:sign_place(l:bt.id, l:bt.file, l:bt.line) else let l:id = len(s:list_breakpoints()) + 1 - exe 'sign place ' . l:id . ' line=' . l:linenr . ' name=godebugbreakpoint file=' . l:filename + call s:sign_place(l:id, l:filename, l:linenr) endif endif catch - call go#util#EchoError(v:exception) + call go#util#EchoError(printf('could not toggle breakpoint: %s', v:exception)) return 1 endtry return 0 endfunction +function! s:sign_unplace(id, file) abort + if !exists('*sign_unplace') + exe 'sign unplace ' . a:id .' file=' . a:file + return + endif + + call sign_unplace('vim-go-debug', {'buffer': a:file, 'id': a:id}) +endfunction + +function! s:sign_place(id, expr, lnum) abort + if !exists('*sign_place') + exe 'sign place ' . a:id . ' line=' . a:lnum . ' name=godebugbreakpoint file=' . a:expr + return + endif + + call sign_place(a:id, 'vim-go-debug', 'godebugbreakpoint', a:expr, {'lnum': a:lnum}) +endfunction + function! s:list_breakpoints() - " :sign place - " --- Signs --- - " Signs for a.go: - " line=15 id=2 name=godebugbreakpoint - " line=16 id=1 name=godebugbreakpoint - " Signs for a_test.go: - " line=6 id=3 name=godebugbreakpoint - - let l:signs = [] - let l:file = '' - for l:line in split(execute('sign place'), '\n')[1:] - if l:line =~# '^Signs for ' - let l:file = l:line[10:-2] - continue - endif - - if l:line !~# 'name=godebugbreakpoint' - continue - endif - - let l:sign = matchlist(l:line, '\vline\=(\d+) +id\=(\d+)') - call add(l:signs, { - \ 'id': l:sign[2], - \ 'file': fnamemodify(l:file, ':p'), - \ 'line': str2nr(l:sign[1]), - \ }) + let l:breakpoints = [] + let l:signs = s:sign_getplaced() + for l:item in l:signs + let l:file = fnamemodify(bufname(l:item.bufnr), ':p') + for l:sign in l:item.signs + call add(l:breakpoints, { + \ 'id': l:sign.id, + \ 'file': l:file, + \ 'line': l:sign.lnum, + \ }) + endfor endfor + return l:breakpoints +endfunction + +function! s:sign_getplaced() abort + if !exists('*sign_getplaced') " sign_getplaced was introduced in Vim 8.1.0614 + " :sign place + " --- Signs --- + " Signs for a.go: + " line=15 id=2 name=godebugbreakpoint + " line=16 id=1 name=godebugbreakpoint + " Signs for a_test.go: + " line=6 id=3 name=godebugbreakpoint + + " l:signs should be the same sam form as the return value for + " sign_getplaced(), a list with the following entries: + " * bufnr - number of the buffer with the sign + " * signs = list of signs placed in bufnr + let l:signs = [] + let l:file = '' + for l:line in split(execute('sign place'), '\n')[1:] + if l:line =~# '^Signs for ' + let l:file = l:line[10:-2] + continue + else + " sign place's output may end with Signs instead of starting with Signs. + " See + " https://github.com/fatih/vim-go/issues/2920#issuecomment-644885774. + let l:idx = match(l:line, '\.go .* Signs:$') + if l:idx >= 0 + let l:file = l:line[0:l:idx+2] + continue + endif + endif + + if l:line !~# 'name=godebugbreakpoint' + continue + endif + + let l:sign = matchlist(l:line, '\vline\=(\d+) +id\=(\d+)') + call add(l:signs, { + \ 'bufnr': bufnr(l:file), + \ 'signs': [{ + \ 'id': str2nr(l:sign[2]), + \ 'lnum': str2nr(l:sign[1]), + \ }], + \ }) + endfor + + return l:signs + endif + + " it would be nice to use lambda's here, but vim-vimparser currently fails + " to parse lamdas as map() arguments. + " TODO(bc): return flatten(map(filter(copy(getbufinfo()), { _, val -> val.listed }), { _, val -> sign_getplaced(val.bufnr, {'group': 'vim-go-debug', 'name': 'godebugbreakpoint'})})) + let l:bufinfo = getbufinfo() + let l:listed = [] + for l:info in l:bufinfo + if l:info.listed + let l:listed = add(l:listed, l:info) + endif + endfor + + let l:signs = [] + for l:buf in l:listed + let l:signs = add(l:signs, sign_getplaced(l:buf.bufnr, {'group': 'vim-go-debug', 'name': 'godebugbreakpoint'})[0]) + endfor return l:signs endfunction -sign define godebugbreakpoint text=> texthl=GoDebugBreakpoint +exe 'sign define godebugbreakpoint text='.go#config#DebugBreakpointSignText().' texthl=GoDebugBreakpoint' sign define godebugcurline text== texthl=GoDebugCurrent linehl=GoDebugCurrent +" s:rpc_response is a convenience function to check for errors and return +" a:res when a:res is not an error response. +function! s:rpc_response(check_errors, res) abort + call a:check_errors() + return a:res +endfunction + +" s:noop is a noop function. It takes any number of arguments and does +" nothing. +function s:noop(...) abort +endfunction + +function! s:warn_when_stale(filename) abort + let l:bufinfo = getbufinfo(a:filename) + if len(l:bufinfo) == 0 + return + endif + + if l:bufinfo[0].changed + call s:warn_stale() + return + endif + + call s:call_jsonrpc(function('s:handle_staleness_check_response', [fnamemodify(a:filename, ':p')]), 'RPCServer.LastModified') +endfunction + +function! s:handle_staleness_check_response(filename, check_errors, res) abort + try + call a:check_errors() + catch + " swallow any errors + return + endtry + + let l:ftime = strftime('%Y-%m-%dT%H:%M:%S', getftime(a:filename)) + if l:ftime < a:res.result.Time[0:(len(l:ftime) - 1)] + return + endif + call s:warn_stale(a:filename) +endfunction + +function! s:warn_stale(filename) abort + call go#util#EchoWarning(printf('file locations may be incorrect, because %s has changed since debugging started', a:filename)) +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/debug_test.vim b/pack/acp/start/vim-go/autoload/go/debug_test.vim index f5572e5..b82f827 100644 --- a/pack/acp/start/vim-go/autoload/go/debug_test.vim +++ b/pack/acp/start/vim-go/autoload/go/debug_test.vim @@ -10,6 +10,10 @@ function! Test_GoDebugStart_RelativePackage() abort call s:debug('./debug/debugmain') endfunction +function! Test_GoDebugStart_RelativePackage_NullModule() abort + call s:debug('./debug/debugmain', 1) +endfunction + function! Test_GoDebugStart_Package() abort call s:debug('debug/debugmain') endfunction @@ -20,20 +24,21 @@ function! Test_GoDebugStart_Errors() abort endif try + let l:tmp = gotest#load_fixture('debug/compilerror/main.go') + let l:expected = [ \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 0, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': '# debug/compilerror'}, - \ {'lnum': 6, 'bufnr': 7, 'col': 22, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': ' syntax error: unexpected newline, expecting comma or )'}, + \ {'lnum': 6, 'bufnr': bufnr('%'), 'col': 22, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': ' syntax error: unexpected newline, expecting comma or )'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 0, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exit status 2'} \] call setqflist([], 'r') - let l:tmp = gotest#load_fixture('debug/compilerror/main.go') call assert_false(exists(':GoDebugStop')) let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' execute l:cd . ' debug/compilerror' - call go#debug#Start(0) + call go#debug#Start('debug') let l:actual = getqflist() let l:start = reltime() @@ -52,14 +57,22 @@ function! Test_GoDebugStart_Errors() abort endtry endfunction +" s:debug takes 2 optional arguments. The first is a package to debug. The +" second is a flag to indicate whether to reset GOPATH after +" gotest#load_fixture is called in order to test behavior outside of GOPATH. function! s:debug(...) abort if !go#util#has_job() return endif try + let $oldgopath = $GOPATH let l:tmp = gotest#load_fixture('debug/debugmain/debugmain.go') + if a:0 > 1 && a:2 == 1 + let $GOPATH = $oldgopath + endif + call go#debug#Breakpoint(6) call assert_false(exists(':GoDebugStop')) @@ -67,9 +80,9 @@ function! s:debug(...) abort if a:0 == 0 let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' execute l:cd . ' debug/debugmain' - let l:job = go#debug#Start(0) + let l:job = go#debug#Start('debug') else - let l:job = go#debug#Start(0, a:1) + let l:job = go#debug#Start('debug', a:1) endif let l:start = reltime() diff --git a/pack/acp/start/vim-go/autoload/go/def.vim b/pack/acp/start/vim-go/autoload/go/def.vim index 5fa1141..b8c1a9d 100644 --- a/pack/acp/start/vim-go/autoload/go/def.vim +++ b/pack/acp/start/vim-go/autoload/go/def.vim @@ -6,7 +6,7 @@ let s:go_stack = [] let s:go_stack_level = 0 function! go#def#Jump(mode, type) abort - let fname = fnamemodify(expand("%"), ':p:gs?\\?/?') + let l:fname = fnamemodify(expand("%"), ':p:gs?\\?/?') " so guru right now is slow for some people. previously we were using " godef which also has it's own quirks. But this issue come up so many @@ -66,7 +66,15 @@ function! go#def#Jump(mode, type) abort let [l:out, l:err] = go#util#ExecInDir(l:cmd) endif elseif bin_name == 'gopls' - let [l:line, l:col] = getpos('.')[1:2] + if !go#config#GoplsEnabled() + call go#util#EchoError("go_def_mode is 'gopls', but gopls is disabled") + return + endif + + " reset l:fname when using gopls so that the filename will be converted to + " a URI correctly on windows. + let l:fname = expand('%') + let [l:line, l:col] = go#lsp#lsp#Position() " delegate to gopls, with an empty job object and an exit status of 0 " (they're irrelevant for gopls). if a:type @@ -162,9 +170,9 @@ function! go#def#jump_to_declaration(out, mode, bin_name) abort if filename != fnamemodify(expand("%"), ':p:gs?\\?/?') " jump to existing buffer if, 1. we have enabled it, 2. the buffer is loaded " and 3. there is buffer window number we switch to - if go#config#DefReuseBuffer() && bufloaded(filename) != 0 && bufwinnr(filename) != -1 - " jumpt to existing buffer if it exists - execute bufwinnr(filename) . 'wincmd w' + if go#config#DefReuseBuffer() && bufwinnr(filename) != -1 + " jump to existing buffer if it exists + call win_gotoid(bufwinid(filename)) else if &modified let cmd = 'hide edit' @@ -186,7 +194,13 @@ function! go#def#jump_to_declaration(out, mode, bin_name) abort endif " open the file and jump to line and column - exec cmd fnameescape(fnamemodify(filename, ':.')) + try + exec cmd fnameescape(fnamemodify(filename, ':.')) + catch + if stridx(v:exception, ':E325:') < 0 + call go#util#EchoError(v:exception) + endif + endtry endif endif call cursor(line, col) diff --git a/pack/acp/start/vim-go/autoload/go/def_test.vim b/pack/acp/start/vim-go/autoload/go/def_test.vim index a054471..110d8c3 100644 --- a/pack/acp/start/vim-go/autoload/go/def_test.vim +++ b/pack/acp/start/vim-go/autoload/go/def_test.vim @@ -2,19 +2,21 @@ let s:cpo_save = &cpo set cpo&vim +scriptencoding utf-8 + func! Test_jump_to_declaration_guru() abort try let l:filename = 'def/jump.go' - let lnum = 5 - let col = 6 + let l:lnum = 5 + let l:col = 6 let l:tmp = gotest#load_fixture(l:filename) - let guru_out = printf("%s:%d:%d: defined here as func main", filename, lnum, col) - call go#def#jump_to_declaration(guru_out, "", 'guru') + let l:guru_out = printf("%s:%d:%d: defined here as func main", l:filename, l:lnum, l:col) + call go#def#jump_to_declaration(l:guru_out, "", 'guru') - call assert_equal(filename, bufname("%")) - call assert_equal(lnum, getcurpos()[1]) - call assert_equal(col, getcurpos()[2]) + call assert_equal(l:filename, bufname("%")) + call assert_equal(l:lnum, getcurpos()[1]) + call assert_equal(l:col, getcurpos()[2]) finally call delete(l:tmp, 'rf') endtry @@ -22,17 +24,17 @@ endfunc func! Test_jump_to_declaration_godef() abort try - let filename = 'def/jump.go' - let lnum = 5 - let col = 6 + let l:filename = 'def/jump.go' + let l:lnum = 5 + let l:col = 6 let l:tmp = gotest#load_fixture(l:filename) - let godef_out = printf("%s:%d:%d\ndefined here as func main", filename, lnum, col) + let l:godef_out = printf("%s:%d:%d\ndefined here as func main", l:filename, l:lnum, l:col) call go#def#jump_to_declaration(godef_out, "", 'godef') - call assert_equal(filename, bufname("%")) - call assert_equal(lnum, getcurpos()[1]) - call assert_equal(col, getcurpos()[2]) + call assert_equal(l:filename, bufname("%")) + call assert_equal(l:lnum, getcurpos()[1]) + call assert_equal(l:col, getcurpos()[2]) finally call delete(l:tmp, 'rf') endtry @@ -40,33 +42,180 @@ endfunc func! Test_Jump_leaves_lists() abort try - let filename = 'def/jump.go' + let l:filename = 'def/jump.go' let l:tmp = gotest#load_fixture(l:filename) - let expected = [{'lnum': 10, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'quux'}] + let l:expected = [{'lnum': 10, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'quux'}] - call setloclist(winnr(), copy(expected), 'r' ) - call setqflist(copy(expected), 'r' ) + call setloclist(0, copy(l:expected), 'r' ) + call setqflist(copy(l:expected), 'r' ) let l:bufnr = bufnr('%') call cursor(6, 7) + + if !go#util#has_job() + let g:go_def_mode='godef' + endif call go#def#Jump('', 0) - let start = reltime() - while bufnr('%') == l:bufnr && reltimefloat(reltime(start)) < 10 + if !go#util#has_job() + unlet g:go_def_mode + endif + + let l:start = reltime() + while bufnr('%') == l:bufnr && reltimefloat(reltime(l:start)) < 10 sleep 100m endwhile - let actual = getloclist(winnr()) - call gotest#assert_quickfix(actual, expected) + let l:actual = getloclist(0) + call gotest#assert_quickfix(l:actual, l:expected) - let actual = getqflist() - call gotest#assert_quickfix(actual, expected) + let l:actual = getqflist() + call gotest#assert_quickfix(l:actual, l:expected) finally call delete(l:tmp, 'rf') endtry endfunc +func! Test_DefJump_gopls_simple_first() abort + if !go#util#has_job() + return + endif + + try + let g:go_def_mode = 'gopls' + + let l:tmp = gotest#write_file('simple/firstposition/firstposition.go', [ + \ 'package firstposition', + \ '', + \ 'func Example() {', + \ "\tid := " . '"foo"', + \ "\tprintln(" . '"id:", id)', + \ '}', + \ ] ) + + let l:expected = [0, 4, 2, 0] + + call assert_notequal(l:expected, getpos('.')) + + call go#def#Jump('', 0) + + let l:start = reltime() + while getpos('.') != l:expected && reltimefloat(reltime(l:start)) < 10 + sleep 100m + endwhile + + call assert_equal(l:expected, getpos('.')) + finally + call delete(l:tmp, 'rf') + unlet g:go_def_mode + endtry +endfunc + +func! Test_DefJump_gopls_simple_last() abort + if !go#util#has_job() + return + endif + + try + let g:go_def_mode = 'gopls' + + let l:tmp = gotest#write_file('simple/lastposition/lastposition.go', [ + \ 'package lastposition', + \ '', + \ 'func Example() {', + \ "\tid := " . '"foo"', + \ "\tprintln(" . '"id:", id)', + \ '}', + \ ] ) + + let l:expected = [0, 4, 2, 0] + + call assert_notequal(l:expected, getpos('.')) + + call go#def#Jump('', 0) + + let l:start = reltime() + while getpos('.') != l:expected && reltimefloat(reltime(l:start)) < 10 + sleep 100m + endwhile + + call assert_equal(l:expected, getpos('.')) + finally + call delete(l:tmp, 'rf') + unlet g:go_def_mode + endtry +endfunc + +func! Test_DefJump_gopls_MultipleCodeUnit_first() abort + if !go#util#has_job() + return + endif + + try + let g:go_def_mode = 'gopls' + + let l:tmp = gotest#write_file('multiplecodeunit/firstposition/firstposition.go', [ + \ 'package firstposition', + \ '', + \ 'func Example() {', + \ "\t𐐀, id := " . '"foo", "bar"', + \ "\tprintln(" . '"(𐐀, id):", 𐐀, id)', + \ '}', + \ ] ) + + let l:expected = [0, 4, 8, 0] + call assert_notequal(l:expected, getpos('.')) + + call go#def#Jump('', 0) + + let l:start = reltime() + while getpos('.') != l:expected && reltimefloat(reltime(l:start)) < 10 + sleep 100m + endwhile + + call assert_equal(l:expected, getpos('.')) + finally + call delete(l:tmp, 'rf') + unlet g:go_def_mode + endtry +endfunc + + +func! Test_DefJump_gopls_MultipleCodeUnit_last() abort + if !go#util#has_job() + return + endif + + try + let g:go_def_mode = 'gopls' + + let l:tmp = gotest#write_file('multiplecodeunit/lastposition/lastposition.go', [ + \ 'package lastposition', + \ '', + \ 'func Example() {', + \ "\t𐐀, id := " . '"foo", "bar"', + \ "\tprintln(" . '"(𐐀, id):", 𐐀, id)', + \ '}', + \ ] ) + + let l:expected = [0, 4, 8, 0] + call assert_notequal(l:expected, getpos('.')) + + call go#def#Jump('', 0) + + let l:start = reltime() + while getpos('.') != l:expected && reltimefloat(reltime(l:start)) < 10 + sleep 100m + endwhile + + call assert_equal(l:expected, getpos('.')) + finally + call delete(l:tmp, 'rf') + unlet g:go_def_mode + endtry +endfunc + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/doc.vim b/pack/acp/start/vim-go/autoload/go/doc.vim index 5449bf7..417c231 100644 --- a/pack/acp/start/vim-go/autoload/go/doc.vim +++ b/pack/acp/start/vim-go/autoload/go/doc.vim @@ -6,36 +6,25 @@ let s:cpo_save = &cpo set cpo&vim +scriptencoding utf-8 + let s:buf_nr = -1 function! go#doc#OpenBrowser(...) abort - " check if we have gogetdoc as it gives us more and accurate information. - " Only supported if we have json_decode as it's not worth to parse the plain - " non-json output of gogetdoc - let bin_path = go#path#CheckBinPath('gogetdoc') - if !empty(bin_path) && exists('*json_decode') - let [l:json_out, l:err] = s:gogetdoc(1) + if len(a:000) == 0 + let [l:out, l:err] = go#lsp#DocLink() if l:err - call go#util#EchoError(json_out) + call go#util#EchoError(l:out) return endif - let out = json_decode(json_out) - if type(out) != type({}) - call go#util#EchoError("gogetdoc output is malformed") + if len(l:out) == 0 + call go#util#EchoWarning("could not path for doc URL") endif - let import = out["import"] - let name = out["name"] - let decl = out["decl"] + let l:godoc_url = printf('%s/%s', go#config#DocUrl(), l:out) - let godoc_url = go#config#DocUrl() - let godoc_url .= "/" . import - if decl !~ "^package" - let godoc_url .= "#" . name - endif - - call go#util#OpenBrowser(godoc_url) + call go#util#OpenBrowser(l:godoc_url) return endif @@ -48,7 +37,7 @@ function! go#doc#OpenBrowser(...) abort let exported_name = pkgs[1] " example url: https://godoc.org/github.com/fatih/set#Set - let godoc_url = go#config#DocUrl() . "/" . pkg . "#" . exported_name + let godoc_url = printf('%s/%s#%s', go#config#DocUrl(), pkg, exported_name) call go#util#OpenBrowser(godoc_url) endfunction @@ -56,11 +45,8 @@ function! go#doc#Open(newmode, mode, ...) abort " With argument: run "godoc [arg]". if len(a:000) let [l:out, l:err] = go#util#Exec(['go', 'doc'] + a:000) - else " Without argument: run gogetdoc on cursor position. - let [l:out, l:err] = s:gogetdoc(0) - if out == -1 - return - endif + else " Without argument: use gopls to get documentation + let [l:out, l:err] = go#lsp#Doc() endif if l:err @@ -68,10 +54,62 @@ function! go#doc#Open(newmode, mode, ...) abort return endif - call s:GodocView(a:newmode, a:mode, out) + call s:GodocView(a:newmode, a:mode, l:out) endfunction function! s:GodocView(newposition, position, content) abort + " popup window + if go#config#DocPopupWindow() + if exists('*popup_atcursor') && exists('*popup_clear') + call popup_clear() + + let borderchars = ['-', '|', '-', '|', '+', '+', '+', '+'] + if &encoding == "utf-8" + let borderchars = ['─', '│', '─', '│', '┌', '┐', '┘', '└'] + endif + call popup_atcursor(split(a:content, '\n'), { + \ 'padding': [1, 1, 1, 1], + \ 'borderchars': borderchars, + \ 'border': [1, 1, 1, 1], + \ }) + elseif has('nvim') && exists('*nvim_open_win') + let lines = split(a:content, '\n') + let height = 0 + let width = 0 + for line in lines + let lw = strdisplaywidth(line) + if lw > width + let width = lw + endif + let height += 1 + endfor + let width += 1 " right margin + let max_height = go#config#DocMaxHeight() + if height > max_height + let height = max_height + endif + + let buf = nvim_create_buf(v:false, v:true) + call nvim_buf_set_lines(buf, 0, -1, v:true, lines) + let opts = { + \ 'relative': 'cursor', + \ 'row': 1, + \ 'col': 0, + \ 'width': width, + \ 'height': height, + \ 'style': 'minimal', + \ } + call nvim_open_win(buf, v:true, opts) + setlocal nomodified nomodifiable filetype=godoc + + " close easily with CR, Esc and q + noremap :close + noremap :close + noremap q :close + endif + return + endif + " reuse existing buffer window if it exists otherwise create a new one let is_visible = bufexists(s:buf_nr) && bufwinnr(s:buf_nr) != -1 if !bufexists(s:buf_nr) @@ -129,23 +167,6 @@ function! s:GodocView(newposition, position, content) abort nnoremap [ [ endfunction -function! s:gogetdoc(json) abort - let l:cmd = [ - \ 'gogetdoc', - \ '-tags', go#config#BuildTags(), - \ '-pos', expand("%:p:gs!\\!/!") . ':#' . go#util#OffsetCursor()] - if a:json - let l:cmd += ['-json'] - endif - - if &modified - let l:cmd += ['-modified'] - return go#util#Exec(l:cmd, go#util#archive()) - endif - - return go#util#Exec(l:cmd) -endfunction - " returns the package and exported name. exported name might be empty. " ie: fmt and Println " ie: github.com/fatih/set and New diff --git a/pack/acp/start/vim-go/autoload/go/fillstruct_test.vim b/pack/acp/start/vim-go/autoload/go/fillstruct_test.vim index 956cd1e..73ed089 100644 --- a/pack/acp/start/vim-go/autoload/go/fillstruct_test.vim +++ b/pack/acp/start/vim-go/autoload/go/fillstruct_test.vim @@ -62,7 +62,7 @@ func! Test_fillstruct_two_line() abort \ '\tAddress: "",', \ '}) }']) finally - "call delete(l:tmp, 'rf') + call delete(l:tmp, 'rf') endtry endfunc diff --git a/pack/acp/start/vim-go/autoload/go/fmt.vim b/pack/acp/start/vim-go/autoload/go/fmt.vim index 908c0f5..d3a7d92 100644 --- a/pack/acp/start/vim-go/autoload/go/fmt.vim +++ b/pack/acp/start/vim-go/autoload/go/fmt.vim @@ -18,6 +18,30 @@ set cpo&vim " this and have VimL experience, please look at the function for " improvements, patches are welcome :) function! go#fmt#Format(withGoimport) abort + let l:bin_name = go#config#FmtCommand() + if a:withGoimport == 1 + let l:mode = go#config#ImportsMode() + if l:mode == 'gopls' + if !go#config#GoplsEnabled() + call go#util#EchoError("go_imports_mode is 'gopls', but gopls is disabled") + return + endif + call go#lsp#Imports() + return + endif + + let l:bin_name = 'goimports' + endif + + if l:bin_name == 'gopls' + if !go#config#GoplsEnabled() + call go#util#EchoError("go_def_mode is 'gopls', but gopls is disabled") + return + endif + call go#lsp#Format() + return + endif + if go#config#FmtExperimental() " Using winsaveview to save/restore cursor state has the problem of " closing folds on save: @@ -52,20 +76,16 @@ function! go#fmt#Format(withGoimport) abort let l:tmpname = tr(l:tmpname, '\', '/') endif - let bin_name = go#config#FmtCommand() - if a:withGoimport == 1 - let bin_name = "goimports" - endif - let current_col = col('.') - let [l:out, l:err] = go#fmt#run(bin_name, l:tmpname, expand('%')) - let diff_offset = len(readfile(l:tmpname)) - line('$') + let [l:out, l:err] = go#fmt#run(l:bin_name, l:tmpname, expand('%')) + let line_offset = len(readfile(l:tmpname)) - line('$') + let l:orig_line = getline('.') if l:err == 0 call go#fmt#update_file(l:tmpname, expand('%')) elseif !go#config#FmtFailSilently() - let errors = s:parse_errors(expand('%'), out) - call s:show_errors(errors) + let l:errors = s:replace_filename(expand('%'), out) + call go#fmt#ShowErrors(l:errors) endif " We didn't use the temp file, so clean up @@ -87,8 +107,10 @@ function! go#fmt#Format(withGoimport) abort call winrestview(l:curw) endif - " be smart and jump to the line the new statement was added/removed - call cursor(line('.') + diff_offset, current_col) + " be smart and jump to the line the new statement was added/removed and + " adjust the column within the line + let l:lineno = line('.') + line_offset + call cursor(l:lineno, current_col + (len(getline(l:lineno)) - len(l:orig_line))) " Syntax highlighting breaks less often. syntax sync fromstart @@ -115,28 +137,12 @@ function! go#fmt#update_file(source, target) " reload buffer to reflect latest changes silent edit! + call go#lsp#DidChange(expand(a:target, ':p')) + let &fileformat = old_fileformat let &syntax = &syntax - let l:listtype = go#list#Type("GoFmt") - - " the title information was introduced with 7.4-2200 - " https://github.com/vim/vim/commit/d823fa910cca43fec3c31c030ee908a14c272640 - if has('patch-7.4.2200') - " clean up previous list - if l:listtype == "quickfix" - let l:list_title = getqflist({'title': 1}) - else - let l:list_title = getloclist(0, {'title': 1}) - endif - else - " can't check the title, so assume that the list was for go fmt. - let l:list_title = {'title': 'Format'} - endif - - if has_key(l:list_title, "title") && l:list_title['title'] == "Format" - call go#list#Clean(l:listtype) - endif + call go#fmt#CleanErrors() endfunction " run runs the gofmt/goimport command for the given source file and returns @@ -169,39 +175,42 @@ function! s:fmt_cmd(bin_name, source, target) return cmd endfunction -" parse_errors parses the given errors and returns a list of parsed errors -function! s:parse_errors(filename, content) abort - let splitted = split(a:content, '\n') +" replace_filename replaces the filename on each line of content with +" a:filename. +function! s:replace_filename(filename, content) abort + let l:errors = split(a:content, '\n') - " list of errors to be put into location list - let errors = [] - for line in splitted - let tokens = matchlist(line, '^\(.\{-}\):\(\d\+\):\(\d\+\)\s*\(.*\)') - if !empty(tokens) - call add(errors,{ - \"filename": a:filename, - \"lnum": tokens[2], - \"col": tokens[3], - \"text": tokens[4], - \ }) - endif - endfor - - return errors + let l:errors = map(l:errors, printf('substitute(v:val, ''^.\{-}:'', ''%s:'', '''')', a:filename)) + return join(l:errors, "\n") endfunction -" show_errors opens a location list and shows the given errors. If the given -" errors is empty, it closes the the location list -function! s:show_errors(errors) abort +function! go#fmt#CleanErrors() abort let l:listtype = go#list#Type("GoFmt") - if !empty(a:errors) - call go#list#Populate(l:listtype, a:errors, 'Format') - echohl Error | echomsg "Gofmt returned error" | echohl None + + " clean up previous list + if l:listtype == "quickfix" + let l:list_title = getqflist({'title': 1}) + else + let l:list_title = getloclist(0, {'title': 1}) endif + if has_key(l:list_title, 'title') && (l:list_title['title'] == 'Format' || l:list_title['title'] == 'GoMetaLinterAutoSave') + call go#list#Clean(l:listtype) + endif +endfunction + +" show_errors opens a location list and shows the given errors. If errors is +" empty, it closes the the location list. +function! go#fmt#ShowErrors(errors) abort + let l:errorformat = '%f:%l:%c:\ %m' + let l:listtype = go#list#Type("GoFmt") + + call go#list#ParseFormat(l:listtype, l:errorformat, a:errors, 'Format', 0) + let l:errors = go#list#Get(l:listtype) + " this closes the window if there are no errors or it opens - " it if there is any - call go#list#Window(l:listtype, len(a:errors)) + " it if there are any. + call go#list#Window(l:listtype, len(l:errors)) endfunction function! go#fmt#ToggleFmtAutoSave() abort diff --git a/pack/acp/start/vim-go/autoload/go/fmt_test.vim b/pack/acp/start/vim-go/autoload/go/fmt_test.vim index b1b740a..d921cfc 100644 --- a/pack/acp/start/vim-go/autoload/go/fmt_test.vim +++ b/pack/acp/start/vim-go/autoload/go/fmt_test.vim @@ -35,7 +35,7 @@ func! Test_update_file() abort endfunc func! Test_goimports() abort - let $GOPATH = 'test-fixtures/fmt/' + let $GOPATH = printf('%s/%s', fnamemodify(getcwd(), ':p'), 'test-fixtures/fmt') let actual_file = tempname() call writefile(readfile("test-fixtures/fmt/src/imports/goimports.go"), actual_file) diff --git a/pack/acp/start/vim-go/autoload/go/guru.vim b/pack/acp/start/vim-go/autoload/go/guru.vim index 8ff3b3f..91d5a65 100644 --- a/pack/acp/start/vim-go/autoload/go/guru.vim +++ b/pack/acp/start/vim-go/autoload/go/guru.vim @@ -232,11 +232,10 @@ function! go#guru#Describe(selected) abort endfunction function! go#guru#DescribeInfo(showstatus) abort - " json_encode() and friends are introduced with this patch (7.4.1304) - " vim: https://groups.google.com/d/msg/vim_dev/vLupTNhQhZ8/cDGIk0JEDgAJ - " nvim: https://github.com/neovim/neovim/pull/4131 + + " check if the version of Vim being tested supports json_decode() if !exists("*json_decode") - call go#util#EchoError("requires 'json_decode'. Update your Vim/Neovim version.") + call go#util#EchoError("GoDescribeInfo requires 'json_decode'. Update your Vim/Neovim version.") return endif @@ -406,43 +405,31 @@ endfunction " Show all refs to entity denoted by selected identifier function! go#guru#Referrers(selected) abort let args = { - \ 'mode': 'referrers', - \ 'format': 'plain', - \ 'selected': a:selected, - \ 'needs_scope': 0, - \ } + \ 'mode': 'referrers', + \ 'format': 'plain', + \ 'selected': a:selected, + \ 'needs_scope': 0, + \ } call s:run_guru(args) endfunction function! go#guru#SameIds(showstatus) abort - " we use matchaddpos() which was introduce with 7.4.330, be sure we have - " it: http://ftp.vim.org/vim/patches/7.4/7.4.330 + " check if the version of Vim being tested supports matchaddpos() if !exists("*matchaddpos") call go#util#EchoError("GoSameIds requires 'matchaddpos'. Update your Vim/Neovim version.") return endif - " json_encode() and friends are introduced with this patch (7.4.1304) - " vim: https://groups.google.com/d/msg/vim_dev/vLupTNhQhZ8/cDGIk0JEDgAJ - " nvim: https://github.com/neovim/neovim/pull/4131 + " check if the version of Vim being tested supports json_decode() if !exists("*json_decode") call go#util#EchoError("GoSameIds requires 'json_decode'. Update your Vim/Neovim version.") return endif - let args = { - \ 'mode': 'what', - \ 'format': 'json', - \ 'selected': -1, - \ 'needs_scope': 0, - \ 'custom_parse': function('s:same_ids_highlight'), - \ } - if !a:showstatus - let args.disable_progress = 1 - endif - - call s:run_guru(args) + let [l:line, l:col] = getpos('.')[1:2] + let [l:line, l:col] = go#lsp#lsp#Position(l:line, l:col) + call go#lsp#SameIDs(0, expand('%:p'), l:line, l:col, funcref('s:same_ids_highlight')) endfunction function! s:same_ids_highlight(exit_val, output, mode) abort @@ -482,18 +469,26 @@ function! s:same_ids_highlight(exit_val, output, mode) abort endif let same_ids = result['sameids'] + " highlight the lines + let l:matches = [] for item in same_ids let pos = split(item, ':') - call matchaddpos('goSameId', [[str2nr(pos[-2]), str2nr(pos[-1]), str2nr(poslen)]]) + let l:matches = add(l:matches, [str2nr(pos[-2]), str2nr(pos[-1]), str2nr(poslen)]) endfor + call go#util#HighlightPositions('goSameId', l:matches) + if go#config#AutoSameids() " re-apply SameIds at the current cursor position at the time the buffer " is redisplayed: e.g. :edit, :GoRename, etc. augroup vim-go-sameids autocmd! * - autocmd BufWinEnter nested call go#guru#SameIds(0) + if has('textprop') + autocmd BufReadPost nested call go#guru#SameIds(0) + else + autocmd BufWinEnter nested call go#guru#SameIds(0) + endif augroup end endif endfunction @@ -501,15 +496,7 @@ endfunction " ClearSameIds returns 0 when it removes goSameId groups and non-zero if no " goSameId groups are found. function! go#guru#ClearSameIds() abort - let l:cleared = 0 - - let m = getmatches() - for item in m - if item['group'] == 'goSameId' - call matchdelete(item['id']) - let l:cleared = 1 - endif - endfor + let l:cleared = go#util#ClearHighlights('goSameId') if !l:cleared return 1 @@ -534,11 +521,11 @@ function! go#guru#AutoToggleSameIds() abort call go#util#EchoProgress("sameids auto highlighting disabled") call go#guru#ClearSameIds() call go#config#SetAutoSameids(0) - return + else + call go#util#EchoSuccess("sameids auto highlighting enabled") + call go#config#SetAutoSameids(1) endif - - call go#util#EchoSuccess("sameids auto highlighting enabled") - call go#config#SetAutoSameids(1) + call go#auto#update_autocmd() endfunction @@ -565,11 +552,11 @@ function! s:parse_guru_output(exit_val, output, title) abort let errformat = "%f:%l.%c-%[%^:]%#:\ %m,%f:%l:%c:\ %m" let l:listtype = go#list#Type("_guru") - call go#list#ParseFormat(l:listtype, errformat, a:output, a:title) + call go#list#ParseFormat(l:listtype, errformat, a:output, a:title, 0) let errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(errors)) -endfun +endfunction function! go#guru#Scope(...) abort if a:0 @@ -602,11 +589,9 @@ function! go#guru#DescribeBalloon() abort return endif - " json_encode() and friends are introduced with this patch (7.4.1304) - " vim: https://groups.google.com/d/msg/vim_dev/vLupTNhQhZ8/cDGIk0JEDgAJ - " nvim: https://github.com/neovim/neovim/pull/4131 + " check if the version of Vim being tested supports json_decode() if !exists("*json_decode") - call go#util#EchoError("requires 'json_decode'. Update your Vim/Neovim version.") + call go#util#EchoError("GoDescribeBalloon requires 'json_decode'. Update your Vim/Neovim version.") return endif diff --git a/pack/acp/start/vim-go/autoload/go/highlight_test.vim b/pack/acp/start/vim-go/autoload/go/highlight_test.vim index 5693871..9360a8f 100644 --- a/pack/acp/start/vim-go/autoload/go/highlight_test.vim +++ b/pack/acp/start/vim-go/autoload/go/highlight_test.vim @@ -98,6 +98,325 @@ function! Test_gomodVersion_incompatible_highlight() abort endtry endfunc +function! Test_numeric_literal_highlight() abort + syntax on + + let tests = { + \ 'lone zero': {'group': 'goDecimalInt', 'value': '0'}, + \ 'integer': {'group': 'goDecimalInt', 'value': '1234567890'}, + \ 'integerGrouped': {'group': 'goDecimalInt', 'value': '1_234_567_890'}, + \ 'integerErrorLeadingUnderscore': {'group': 'goDecimalError', 'value': '_1234_567_890'}, + \ 'integerErrorTrailingUnderscore': {'group': 'goDecimalError', 'value': '1_234_567890_'}, + \ 'integerErrorDoubleUnderscore': {'group': 'goDecimalError', 'value': '1_234__567_890'}, + \ 'hexadecimal': {'group': 'goHexadecimalInt', 'value': '0x0123456789abdef'}, + \ 'hexadecimalGrouped': {'group': 'goHexadecimalInt', 'value': '0x012_345_678_9ab_def'}, + \ 'hexadecimalErrorLeading': {'group': 'goHexadecimalError', 'value': '0xg0123456789abdef'}, + \ 'hexadecimalErrorTrailing': {'group': 'goHexadecimalError', 'value': '0x0123456789abdefg'}, + \ 'hexadecimalErrorDoubleUnderscore': {'group': 'goHexadecimalError', 'value': '0x__0123456789abdef'}, + \ 'hexadecimalErrorTrailingUnderscore': {'group': 'goHexadecimalError', 'value': '0x0123456789abdef_'}, + \ 'heXadecimal': {'group': 'goHexadecimalInt', 'value': '0X0123456789abdef'}, + \ 'heXadecimalErrorLeading': {'group': 'goHexadecimalError', 'value': '0Xg0123456789abdef'}, + \ 'heXadecimalErrorTrailing': {'group': 'goHexadecimalError', 'value': '0X0123456789abdefg'}, + \ 'octal': {'group': 'goOctalInt', 'value': '01234567'}, + \ 'octalPrefix': {'group': 'goOctalInt', 'value': '0o1234567'}, + \ 'octalGrouped': {'group': 'goOctalInt', 'value': '0o1_234_567'}, + \ 'octalErrorLeading': {'group': 'goOctalError', 'value': '081234567'}, + \ 'octalErrorTrailing': {'group': 'goOctalError', 'value': '012345678'}, + \ 'octalErrorDoubleUnderscore': {'group': 'goOctalError', 'value': '0o__1234567'}, + \ 'octalErrorTrailingUnderscore': {'group': 'goOctalError', 'value': '0o_123456_7_'}, + \ 'octalErrorTrailingO': {'group': 'goOctalError', 'value': '0o_123456_7o'}, + \ 'octalErrorTrailingX': {'group': 'goOctalError', 'value': '0o_123456_7x'}, + \ 'octalErrorTrailingB': {'group': 'goOctalError', 'value': '0o_123456_7b'}, + \ 'OctalPrefix': {'group': 'goOctalInt', 'value': '0O1234567'}, + \ 'binaryInt': {'group': 'goBinaryInt', 'value': '0b0101'}, + \ 'binaryIntGrouped': {'group': 'goBinaryInt', 'value': '0b_01_01'}, + \ 'binaryErrorLeading': {'group': 'goBinaryError', 'value': '0b20101'}, + \ 'binaryErrorTrailing': {'group': 'goBinaryError', 'value': '0b01012'}, + \ 'binaryErrorDoubleUnderscore': {'group': 'goBinaryError', 'value': '0b_01__01'}, + \ 'binaryOverrideOctal': {'group': 'goBinaryError', 'value': '0b1234567'}, + \ 'binaryErrorTrailingUnderscore': {'group': 'goBinaryError', 'value': '0b_01_01_'}, + \ 'BinaryInt': {'group': 'goBinaryInt', 'value': '0B0101'}, + \ 'BinaryErrorLeading': {'group': 'goBinaryError', 'value': '0B20101'}, + \ 'BinaryErrorTrailing': {'group': 'goBinaryError', 'value': '0B01012'}, + \ } + + for kv in items(tests) + let l:actual = s:numericHighlightGroupInAssignment(kv[0], kv[1].value) + call assert_equal(kv[1].group, l:actual, kv[0]) + endfor +endfunction + +function! Test_zero_as_index_element() abort + syntax on + + let l:actual = s:numericHighlightGroupInSliceElement('zero-element', '0') + call assert_equal('goDecimalInt', l:actual) + let l:actual = s:numericHighlightGroupInMultidimensionalSliceElement('zero-element', '0') + call assert_equal('goDecimalInt', l:actual, 'multi-dimensional') +endfunction + +function! Test_zero_as_slice_index() abort + syntax on + + let l:actual = s:numericHighlightGroupInSliceIndex('zero-index', '0') + call assert_equal('goDecimalInt', l:actual) + let l:actual = s:numericHighlightGroupInMultidimensionalSliceIndex('zero-index', '0', '0') + + call assert_equal('goDecimalInt', l:actual, 'multi-dimensional') +endfunction + +function! Test_zero_as_start_slicing_slice() abort + syntax on + + let l:actual = s:numericHighlightGroupInSliceSlicing('slice-slicing', '0', '1') + call assert_equal('goDecimalInt', l:actual) +endfunction + +function! s:numericHighlightGroupInAssignment(testname, value) + let l:dir = gotest#write_file(printf('numeric/%s.go', a:testname), [ + \ 'package numeric', + \ '', + \ printf("var v = %s\x1f", a:value), + \ ]) + + try + let l:pos = getcurpos() + let l:actual = synIDattr(synID(l:pos[1], l:pos[2], 1), 'name') + return l:actual + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:numericHighlightGroupInSliceElement(testname, value) + let l:dir = gotest#write_file(printf('numeric/slice-element/%s.go', a:testname), [ + \ 'package numeric', + \ '', + \ printf("v := []int{\x1f%s}", a:value), + \ ]) + + try + let l:pos = getcurpos() + let l:actual = synIDattr(synID(l:pos[1], l:pos[2], 1), 'name') + return l:actual + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:numericHighlightGroupInMultidimensionalSliceElement(testname, value) + let l:dir = gotest#write_file(printf('numeric/slice-multidimensional-element/%s.go', a:testname), [ + \ 'package numeric', + \ '', + \ printf("v := [][]int{{\x1f%s},{%s}}", a:value, a:value), + \ ]) + + try + let l:pos = getcurpos() + let l:actual = synIDattr(synID(l:pos[1], l:pos[2], 1), 'name') + return l:actual + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:numericHighlightGroupInSliceIndex(testname, value) + let l:dir = gotest#write_file(printf('numeric/slice-index/%s.go', a:testname), [ + \ 'package numeric', + \ '', + \ 'var sl []int', + \ printf("println(sl[\x1f%s])", a:value), + \ ]) + + try + let l:pos = getcurpos() + let l:actual = synIDattr(synID(l:pos[1], l:pos[2], 1), 'name') + return l:actual + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:numericHighlightGroupInMultidimensionalSliceIndex(testname, first, second) + let l:dir = gotest#write_file(printf('numeric/slice-multidimensional-index/%s.go', a:testname), [ + \ 'package numeric', + \ '', + \ 'var sl [][]int', + \ printf("println(sl[\x1f%s][%s])", a:first, a:second), + \ ]) + + try + let l:pos = getcurpos() + let l:actual = synIDattr(synID(l:pos[1], l:pos[2], 1), 'name') + return l:actual + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:numericHighlightGroupInSliceSlicing(testname, from, to) + let l:dir = gotest#write_file(printf('numeric/slice-slicing/%s.go', a:testname), [ + \ 'package numeric', + \ '', + \ 'var sl = []int{1,2}', + \ printf("println(sl[\x1f%s:%s])", a:from, a:to), + \ ]) + try + let l:pos = getcurpos() + let l:actual = synIDattr(synID(l:pos[1], l:pos[2], 1), 'name') + return l:actual + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! Test_diagnostic_after_fmt() abort + let g:go_fmt_command = 'gofmt' + try + call s:diagnostic_after_write( [ + \ 'package main', + \ 'import "fmt"', + \ '', + \ 'func main() {', + \ '', + \ "\tfmt.Println(\x1fhello)", + \ '}', + \ ], []) + finally + unlet g:go_fmt_command + endtry +endfunction + +function! Test_diagnostic_after_fmt_change() abort + " craft a file that will be changed when its written (gofmt will change it). + let g:go_fmt_command = 'gofmt' + try + call s:diagnostic_after_write( [ + \ 'package main', + \ 'import "fmt"', + \ '', + \ 'func main() {', + \ '', + \ "fmt.Println(\x1fhello)", + \ '}', + \ ], []) + finally + unlet g:go_fmt_command + endtry +endfunction + +function! Test_diagnostic_after_fmt_cleared() abort + " craft a file that will be fixed when it is written. + let g:go_fmt_command = 'gofmt' + try + call s:diagnostic_after_write( [ + \ 'package main', + \ 'import "fmt"', + \ '', + \ 'func main() {', + \ '', + \ "fmt.Println(\x1fhello)", + \ '}', + \ ], ['hello := "hello, vim-go"']) + finally + unlet g:go_fmt_command + endtry +endfunction + +function! Test_diagnostic_after_reload() abort + let l:dir = gotest#write_file('diagnostic/after-reload.go', [ + \ 'package main', + \ 'import "fmt"', + \ '', + \ 'func main() {', + \ '', + \ "\tfmt.Println(\x1fhello)", + \ '}', + \ ]) + try + call s:check_diagnostics('', 'goDiagnosticError', 'initial') + let l:pos = getcurpos() + edit + call setpos('.', l:pos) + call s:check_diagnostics('', 'goDiagnosticError', 'after-reload') + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:diagnostic_after_write(contents, changes) abort + syntax on + + let l:dir = gotest#write_file('diagnostic/after-write.go', a:contents) + + try + let l:pos = getcurpos() + call s:check_diagnostics('', 'goDiagnosticError', 'initial') + + " write a:changes to the previous line and make sure l:actual and + " l:expected are set so that they won't accidentally match on the next + " check. + if len(a:changes) > 0 + call append(l:pos[1]-1, a:changes) + let l:actual = 'goDiagnosticError' + let l:expected = '' + else + let l:actual = '' + let l:expected = 'goDiagnosticError' + endif + + write + + call s:check_diagnostics(l:actual, l:expected, 'after-write') + finally + call delete(l:dir, 'rf') + endtry +endfunction + +function! s:check_diagnostics(actual, expected, when) + let l:actual = a:actual + let l:start = reltime() + + while l:actual != a:expected && reltimefloat(reltime(l:start)) < 10 + " Get the cursor position on each iteration, because the cursor postion + " may change between iterations when go#fmt#GoFmt formats, reloads the + " file, and moves the cursor to try to keep it where the user expects it + " to be when gofmt modifies the files. + let l:pos = getcurpos() + if !has('textprop') + let l:matches = getmatches() + if len(l:matches) == 0 + let l:actual = '' + endif + + for l:m in l:matches + let l:matchline = l:m.pos1[0] + if len(l:m.pos1) < 2 + continue + endif + let l:matchcol = get(l:m.pos1, 1, 1) + if l:pos[1] == l:matchline && l:pos[2] >= l:matchcol && l:pos[2] <= l:matchcol + l:m.pos1[2] + " Ideally, we'd check that the cursor is within the match, but when a + " tab is added on the current line, the cursor position within the + " line will stay constant while the line itself is shifted over by a + " column, so just check the line itself instead of checking a precise + " cursor location. + " if l:pos[1] == l:matchline + let l:actual = l:m.group + break + endif + endfor + + sleep 100m + continue + endif + + let l:actual = get(prop_list(l:pos[1]), 0, {'type': ''}).type + sleep 100m + endwhile + + call assert_equal(a:expected, l:actual, a:when) +endfunction " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/impl_test.vim b/pack/acp/start/vim-go/autoload/go/impl_test.vim index 0809836..6be39a1 100644 --- a/pack/acp/start/vim-go/autoload/go/impl_test.vim +++ b/pack/acp/start/vim-go/autoload/go/impl_test.vim @@ -12,7 +12,7 @@ func! Test_impl() abort call go#impl#Impl('r', 'reader', 'io.Reader') call gotest#assert_buffer(1, [ \ 'func (r reader) Read(p []byte) (n int, err error) {', - \ ' panic("not implemented")', + \ ' panic("not implemented") // TODO: Implement', \ '}']) finally call delete(l:tmp, 'rf') @@ -33,7 +33,7 @@ func! Test_impl_get() abort \ 'type reader struct {}', \ '', \ 'func (r *reader) Read(p []byte) (n int, err error) {', - \ ' panic("not implemented")', + \ ' panic("not implemented") // TODO: Implement', \ '}']) finally call delete(l:tmp, 'rf') diff --git a/pack/acp/start/vim-go/autoload/go/implements.vim b/pack/acp/start/vim-go/autoload/go/implements.vim new file mode 100644 index 0000000..0d25f8c --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/implements.vim @@ -0,0 +1,44 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +function! go#implements#Implements(selected) abort + let l:mode = go#config#ImplementsMode() + if l:mode == 'guru' + call go#guru#Implements(a:selected) + return + elseif l:mode == 'gopls' + if !go#config#GoplsEnabled() + call go#util#EchoError("go_implements_mode is 'gopls', but gopls is disabled") + endif + let [l:line, l:col] = getpos('.')[1:2] + let [l:line, l:col] = go#lsp#lsp#Position(l:line, l:col) + let l:fname = expand('%:p') + call go#lsp#Implements(l:fname, l:line, l:col, funcref('s:parse_output')) + return + else + call go#util#EchoWarning('unknown value for g:go_implements_mode') + endif +endfunction + +" This uses Vim's errorformat to parse the output and put it into a quickfix +" or locationlist. +function! s:parse_output(exit_val, output, title) abort + if a:exit_val + call go#util#EchoError(a:output) + return + endif + + let errformat = ",%f:%l:%c:\ %m" + let l:listtype = go#list#Type("GoImplements") + call go#list#ParseFormat(l:listtype, errformat, a:output, a:title, 0) + + let errors = go#list#Get(l:listtype) + call go#list#Window(l:listtype, len(errors)) +endfunction + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/import.vim b/pack/acp/start/vim-go/autoload/go/import.vim index 4e84896..848a5c0 100644 --- a/pack/acp/start/vim-go/autoload/go/import.vim +++ b/pack/acp/start/vim-go/autoload/go/import.vim @@ -82,7 +82,7 @@ function! go#import#SwitchImport(enabled, localname, path, bang) abort while line <= line("$") let line = line + 1 let linestr = getline(line) - let m = matchlist(getline(line), '^\()\|\(\s\+\)\(\S*\s*\)"\(.\+\)"\)') + let m = matchlist(getline(line), '^\()\|\(\s\+\)\(\w\+\s\+\)\="\(.\+\)"\)') if empty(m) if siteprefix == "" && a:enabled " must be in the first group diff --git a/pack/acp/start/vim-go/autoload/go/import_test.vim b/pack/acp/start/vim-go/autoload/go/import_test.vim new file mode 100644 index 0000000..c4306a1 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/import_test.vim @@ -0,0 +1,35 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +func! Test_SwitchImportAddIgnoresCommented() + try + let l:tmp = gotest#write_file('import/import.go', [ + \ 'package import', + \ '', + \ 'import (', + \ "\t" . '// "fmt"', + \ "\t" . '"io"', + \ "\t" . '"ioutil"', + \ "\t" . '"os"', + \ ')', + \ '', + \ 'func main() {', + \ ' io.Copy(ioutil.Discard, os.Stdin)', + \ ' fmt.Println("import the package")', + \ '}', + \ ]) + call go#import#SwitchImport(1, '', 'fmt', 0) + + let l:actual = getline(4) + call assert_equal("\t" . '"fmt"', l:actual) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/issue.vim b/pack/acp/start/vim-go/autoload/go/issue.vim index 65db9d8..135cc25 100644 --- a/pack/acp/start/vim-go/autoload/go/issue.vim +++ b/pack/acp/start/vim-go/autoload/go/issue.vim @@ -18,20 +18,34 @@ function! s:issuebody() abort for l in lines let body = add(body, l) - if l =~ '^\* Vim version' - redir => out - silent version - redir END + if l =~ '^' let [out, err] = go#util#Exec(['go', 'version']) let body = add(body, substitute(l:out, rtrimpat, '', '')) - elseif l =~ '^\* Go environment' - let [out, err] = go#util#Exec(['go', 'env']) + elseif l =~ '^' + let [out, err] = go#util#ExecInDir(['go', 'env']) + let body = add(body, substitute(l:out, rtrimpat, '', '')) + elseif l=~ '^' + let [out, err] = go#util#Exec(['gopls', 'version']) let body = add(body, substitute(l:out, rtrimpat, '', '')) endif endfor + let body = add(body, "\n#### vim-go configuration:\n

vim-go configuration
")
+
+  for k in keys(g:)
+    if k =~ '^go_'
+      let body = add(body, 'g:' . k . ' = ' . string(get(g:, k)))
+    endif
+  endfor
+
+  let body = add(body, '
') + + let body = add(body, printf("\n#### filetype detection configuration:\n
filetype detection
%s", execute('filetype')))
+  let body = add(body, '
') + return join(body, "\n") endfunction diff --git a/pack/acp/start/vim-go/autoload/go/job.vim b/pack/acp/start/vim-go/autoload/go/job.vim index 328572e..f0ee305 100644 --- a/pack/acp/start/vim-go/autoload/go/job.vim +++ b/pack/acp/start/vim-go/autoload/go/job.vim @@ -41,7 +41,12 @@ endfunction " the current window will be the window that was hosting the buffer when " the job was started. After it returns, the current window will be " restored to what it was before the function was called. - +" 'preserveerrors': +" A function that will be passed one value, the list type. It should +" return a boolean value that indicates whether any errors encountered +" should be consider additive to the existing set of errors. This is +" mostly useful for a set of commands that are run via autocmds. +" " The return value is a dictionary with these keys: " 'callback': " A function suitable to be passed as a job callback handler. See @@ -61,7 +66,7 @@ function! go#job#Options(args) let state = { \ 'winid': win_getid(winnr()), \ 'dir': getcwd(), - \ 'jobdir': fnameescape(expand("%:p:h")), + \ 'jobdir': expand("%:p:h"), \ 'messages': [], \ 'bang': 0, \ 'for': "_job", @@ -69,12 +74,10 @@ function! go#job#Options(args) \ 'exit_status': 0, \ 'closed': 0, \ 'errorformat': &errorformat, - \ 'statustype' : '' + \ 'statustype' : '', \ } - if has("patch-8.0.0902") || has('nvim') - let cbs.cwd = state.jobdir - endif + let cbs.cwd = state.jobdir if has_key(a:args, 'bang') let state.bang = a:args.bang @@ -92,6 +95,10 @@ function! go#job#Options(args) let state.errorformat = a:args.errorformat endif + if has_key(a:args, 'preserveerrors') + let state.preserveerrors = a:args.preserveerrors + endif + function state.complete(job, exit_status, data) if has_key(self, 'custom_complete') let l:winid = win_getid(winnr()) @@ -177,16 +184,26 @@ function! go#job#Options(args) call win_gotoid(self.winid) let l:listtype = go#list#Type(self.for) + + let l:preserveerrors = 0 + if has_key(self, 'preserveerrors') + let l:preserveerrors = self.preserveerrors(l:listtype) + endif + if a:exit_status == 0 - call go#list#Clean(l:listtype) - call win_gotoid(l:winid) + if !l:preserveerrors + call go#list#Clean(l:listtype) + call win_gotoid(l:winid) + endif return endif let l:listtype = go#list#Type(self.for) if len(a:data) == 0 - call go#list#Clean(l:listtype) - call win_gotoid(l:winid) + if !l:preserveerrors + call go#list#Clean(l:listtype) + call win_gotoid(l:winid) + endif return endif @@ -195,8 +212,8 @@ function! go#job#Options(args) let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' try " parse the errors relative to self.jobdir - execute l:cd self.jobdir - call go#list#ParseFormat(l:listtype, self.errorformat, out, self.for) + execute l:cd fnameescape(self.jobdir) + call go#list#ParseFormat(l:listtype, self.errorformat, out, self.for, l:preserveerrors) let errors = go#list#Get(l:listtype) finally execute l:cd fnameescape(self.dir) @@ -295,11 +312,6 @@ function! go#job#Start(cmd, options) let l:manualcd = 1 let dir = getcwd() execute l:cd fnameescape(filedir) - elseif !(has("patch-8.0.0902") || has('nvim')) - let l:manualcd = 1 - let l:dir = l:options.cwd - execute l:cd fnameescape(l:dir) - call remove(l:options, 'cwd') endif if has_key(l:options, '_start') diff --git a/pack/acp/start/vim-go/autoload/go/job_test.vim b/pack/acp/start/vim-go/autoload/go/job_test.vim new file mode 100644 index 0000000..42f90ad --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/job_test.vim @@ -0,0 +1,53 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +func! Test_JobDirWithSpaces() + if !go#util#has_job() + return + endif + + try + let l:filename = 'job/dir has spaces/main.go' + let l:tmp = gotest#load_fixture(l:filename) + exe 'cd ' . fnameescape(l:tmp . '/src/job/dir has spaces') + + " set the compiler type so that the errorformat option will be set + " correctly. + compiler go + + let expected = [{'lnum': 4, 'bufnr': bufnr('%'), 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'undefined: notafunc'}] + " clear the quickfix lists + call setqflist([], 'r') + + " go build discards any results when it compiles multiple packages. So we + " pass the `errors` package just as a placeholder with the current folder + " (indicated with '.'). + let l:cmd = ['go', 'build', '.', 'errors'] + + let l:complete = go#promise#New(function('s:complete'), 10000, '') + call go#job#Spawn(l:cmd, { + \ 'for': 'GoBuild', + \ 'complete': l:complete.wrapper, + \ 'statustype': 'build' + \}) + + let l:out = l:complete.await() + + let actual = getqflist() + + call gotest#assert_quickfix(actual, l:expected) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! s:complete(job, exit_code, messages) + return a:messages +endfunc + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/lint.vim b/pack/acp/start/vim-go/autoload/go/lint.vim index 21e2102..248e666 100644 --- a/pack/acp/start/vim-go/autoload/go/lint.vim +++ b/pack/acp/start/vim-go/autoload/go/lint.vim @@ -3,30 +3,36 @@ let s:cpo_save = &cpo set cpo&vim function! go#lint#Gometa(bang, autosave, ...) abort - if a:0 == 0 - let goargs = [expand('%:p:h')] - else - let goargs = a:000 - endif - let l:metalinter = go#config#MetalinterCommand() - if l:metalinter == 'gometalinter' || l:metalinter == 'golangci-lint' - let cmd = s:metalintercmd(l:metalinter) + if a:0 == 0 + let l:goargs = [expand('%:p:h')] + if l:metalinter == 'gopls' + let l:pkg = go#package#ImportPath() + if l:pkg == -1 + call go#util#EchoError('could not determine package name') + return + endif + + let l:goargs = [l:pkg] + endif + else + let l:goargs = a:000 + endif + + let cmd = [] + if l:metalinter == 'golangci-lint' + let linters = a:autosave ? go#config#MetalinterAutosaveEnabled() : go#config#MetalinterEnabled() + let cmd = s:metalintercmd(l:metalinter, len(linters) != 0) if empty(cmd) return endif - " linters - let linters = a:autosave ? go#config#MetalinterAutosaveEnabled() : go#config#MetalinterEnabled() + " add linters to cmd for linter in linters let cmd += ["--enable=".linter] endfor - - for linter in go#config#MetalinterDisabled() - let cmd += ["--disable=".linter] - endfor - else + elseif l:metalinter != 'gopls' " the user wants something else, let us use it. let cmd = split(go#config#MetalinterCommand(), " ") endif @@ -36,15 +42,8 @@ function! go#lint#Gometa(bang, autosave, ...) abort " will be cleared redraw - if l:metalinter == "gometalinter" - " Include only messages for the active buffer for autosave. - let include = [printf('--include=^%s:.*$', fnamemodify(expand('%:p'), ":."))] - if go#util#has_job() - let include = [printf('--include=^%s:.*$', expand('%:p:t'))] - endif - let cmd += include - elseif l:metalinter == "golangci-lint" - let goargs[0] = expand('%:p') + if l:metalinter == "golangci-lint" + let l:goargs[0] = expand('%:p:h') endif endif @@ -54,139 +53,288 @@ function! go#lint#Gometa(bang, autosave, ...) abort let cmd += ["--deadline=" . deadline] endif - let cmd += goargs + let cmd += l:goargs - if l:metalinter == "gometalinter" - " Gometalinter can output one of the two, so we look for both: - " :::: () - " :::: () - " This can be defined by the following errorformat: - let errformat = "%f:%l:%c:%t%*[^:]:\ %m,%f:%l::%t%*[^:]:\ %m" + let errformat = s:errorformat(l:metalinter) + + if l:metalinter == 'gopls' + if a:autosave + let l:messages = go#lsp#AnalyzeFile(expand('%:p')) + else + let l:import_paths = l:goargs + let l:messages = call('go#lsp#Diagnostics', l:import_paths) + endif + + let l:err = len(l:messages) else - " Golangci-lint can output the following: - " ::: () - " This can be defined by the following errorformat: - let errformat = "%f:%l:%c:\ %m" - endif + if go#util#has_job() + if a:autosave + let l:for = 'GoMetaLinterAutoSave' + else + let l:for = 'GoMetaLinter' + endif - if go#util#has_job() - call s:lint_job({'cmd': cmd, 'statustype': l:metalinter, 'errformat': errformat}, a:bang, a:autosave) - return - endif + call s:lint_job(l:metalinter, {'cmd': cmd, 'statustype': l:metalinter, 'errformat': errformat, 'for': l:for}, a:bang, a:autosave) + return + endif - let [l:out, l:err] = go#util#Exec(cmd) + let [l:out, l:err] = go#util#Exec(cmd) + let l:messages = split(out, "\n") + endif if a:autosave - let l:listtype = go#list#Type("GoMetaLinterAutoSave") + let l:listtype = go#list#Type('GoMetaLinterAutoSave') + let l:for = 'GoMetaLinterAutoSave' else - let l:listtype = go#list#Type("GoMetaLinter") + let l:listtype = go#list#Type('GoMetaLinter') + let l:for = 'GoMetaLinterAuto' endif if l:err == 0 - call go#list#Clean(l:listtype) - echon "vim-go: " | echohl Function | echon "[metalinter] PASS" | echohl None + if !s:preserveerrors(a:autosave, l:listtype) + call go#list#Clean(l:listtype) + endif + call go#util#EchoSuccess('[metalinter] PASS') else let l:winid = win_getid(winnr()) " Parse and populate our location list - call go#list#ParseFormat(l:listtype, errformat, split(out, "\n"), 'GoMetaLinter') + + if a:autosave + call s:metalinterautosavecomplete(l:metalinter, fnamemodify(expand('%:p'), ":."), 0, 1, l:messages) + endif + call go#list#ParseFormat(l:listtype, errformat, l:messages, l:for, s:preserveerrors(a:autosave, l:listtype)) let errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(errors)) if a:autosave || a:bang call win_gotoid(l:winid) + else + call go#list#JumpToFirst(l:listtype) + endif + endif +endfunction + +function! go#lint#Diagnostics(bang, ...) abort + if a:0 == 0 + let l:pkg = go#package#ImportPath() + if l:pkg == -1 + call go#util#EchoError('could not determine package name') return endif - call go#list#JumpToFirst(l:listtype) + + let l:import_paths = [l:pkg] + else + let l:import_paths = a:000 + endif + + let errformat = s:errorformat('gopls') + + let l:messages = call('go#lsp#Diagnostics', l:import_paths) + + let l:listtype = go#list#Type("GoDiagnostics") + + if len(l:messages) == 0 + call go#list#Clean(l:listtype) + call go#util#EchoSuccess('[diagnostics] PASS') + else + " Parse and populate the quickfix list + let l:winid = win_getid(winnr()) + call go#list#ParseFormat(l:listtype, errformat, l:messages, 'GoDiagnostics', 0) + + let errors = go#list#Get(l:listtype) + call go#list#Window(l:listtype, len(errors)) + + if a:bang + call win_gotoid(l:winid) + else + call go#list#JumpToFirst(l:listtype) + endif endif endfunction " Golint calls 'golint' on the current directory. Any warnings are populated in " the location list function! go#lint#Golint(bang, ...) abort + call go#cmd#autowrite() + + let l:type = 'golint' + let l:status = { + \ 'desc': 'current status', + \ 'type': l:type, + \ 'state': "started", + \ } + if go#config#EchoCommandInfo() + call go#util#EchoProgress(printf('[%s] analyzing...', l:type)) + endif + call go#statusline#Update(expand('%:p:h'), l:status) + if a:0 == 0 - let [l:out, l:err] = go#util#Exec([go#config#GolintBin(), go#package#ImportPath()]) + let [l:out, l:err] = go#util#Exec([go#config#GolintBin(), expand('%:p:h')]) else let [l:out, l:err] = go#util#Exec([go#config#GolintBin()] + a:000) endif - if empty(l:out) - call go#util#EchoSuccess('[lint] PASS') - return - endif - - let l:winid = win_getid(winnr()) + let l:status.state = 'success' + let l:state = 'PASS' let l:listtype = go#list#Type("GoLint") - call go#list#Parse(l:listtype, l:out, "GoLint") - let l:errors = go#list#Get(l:listtype) - call go#list#Window(l:listtype, len(l:errors)) + if !empty(l:out) + let l:status.state = 'failed' + let l:state = 'FAIL' - if a:bang - call win_gotoid(l:winid) - return + let l:winid = win_getid(winnr()) + call go#list#Parse(l:listtype, l:out, "GoLint", 0) + let l:errors = go#list#Get(l:listtype) + call go#list#Window(l:listtype, len(l:errors)) + + if a:bang + call win_gotoid(l:winid) + else + call go#list#JumpToFirst(l:listtype) + endif + if go#config#EchoCommandInfo() + call go#util#EchoError(printf('[%s] %s', l:type, l:state)) + endif + else + call go#list#Clean(l:listtype) + if go#config#EchoCommandInfo() + call go#util#EchoSuccess(printf('[%s] %s', l:type, l:state)) + endif endif - - call go#list#JumpToFirst(l:listtype) + call go#statusline#Update(expand('%:p:h'), l:status) endfunction -" Vet calls 'go vet' on the current directory. Any warnings are populated in -" the location list +" Vet calls 'go vet' on the current buffer's directory. Any warnings are +" populated in the location list function! go#lint#Vet(bang, ...) abort call go#cmd#autowrite() - if go#config#EchoCommandInfo() - call go#util#EchoProgress('calling vet...') + let l:cmd = ['go', 'vet'] + + let buildtags = go#config#BuildTags() + if buildtags isnot '' + let l:cmd += ['-tags', buildtags] endif if a:0 == 0 - let [l:out, l:err] = go#util#Exec(['go', 'vet', go#package#ImportPath()]) + let l:import_path = go#package#ImportPath() + if l:import_path == -1 + call go#util#EchoError('could not determine package') + return + endif + let l:cmd = add(l:cmd, l:import_path) else - let [l:out, l:err] = go#util#Exec(['go', 'tool', 'vet'] + a:000) + let l:cmd = extend(l:cmd, a:000) endif + let l:type = 'go vet' + if go#config#EchoCommandInfo() + call go#util#EchoProgress(printf('[%s] analyzing...', l:type)) + endif + let l:status = { + \ 'desc': 'current status', + \ 'type': l:type, + \ 'state': "started", + \ } + call go#statusline#Update(expand('%:p:h'), l:status) + + let [l:out, l:err] = go#util#ExecInDir(l:cmd) + + let l:status.state = 'success' + let l:state = 'PASS' + let l:listtype = go#list#Type("GoVet") if l:err != 0 + let l:status.state = 'failed' + let l:state = 'FAIL' + let l:winid = win_getid(winnr()) - let errorformat = "%-Gexit status %\\d%\\+," . &errorformat - call go#list#ParseFormat(l:listtype, l:errorformat, out, "GoVet") - let errors = go#list#Get(l:listtype) - call go#list#Window(l:listtype, len(errors)) - if !empty(errors) && !a:bang + let l:errorformat = "%-Gexit status %\\d%\\+," . &errorformat + let l:dir = go#util#Chdir(expand('%:p:h')) + try + call go#list#ParseFormat(l:listtype, l:errorformat, out, "GoVet", 0) + finally + call go#util#Chdir(l:dir) + endtry + let l:errors = go#list#Get(l:listtype) + + if empty(l:errors) + call go#util#EchoError(l:out) + return + endif + + call go#list#Window(l:listtype, len(l:errors)) + if !empty(l:errors) && !a:bang call go#list#JumpToFirst(l:listtype) else call win_gotoid(l:winid) endif + + if go#config#EchoCommandInfo() + call go#util#EchoError(printf('[%s] %s', l:type, l:state)) + endif else call go#list#Clean(l:listtype) - call go#util#EchoSuccess('[vet] PASS') + if go#config#EchoCommandInfo() + call go#util#EchoSuccess(printf('[%s] %s', l:type, l:state)) + endif endif + call go#statusline#Update(expand('%:p:h'), l:status) endfunction " ErrCheck calls 'errcheck' for the given packages. Any warnings are populated in " the location list function! go#lint#Errcheck(bang, ...) abort - if a:0 == 0 - let l:import_path = go#package#ImportPath() - if import_path == -1 - call go#util#EchoError('package is not inside GOPATH src') - return - endif - else - let l:import_path = join(a:000, ' ') + call go#cmd#autowrite() + + let l:cmd = [go#config#ErrcheckBin(), '-abspath'] + + let buildtags = go#config#BuildTags() + if buildtags isnot '' + let l:cmd += ['-tags', buildtags] endif - call go#util#EchoProgress('[errcheck] analysing ...') + if a:0 == 0 + let l:import_path = go#package#ImportPath() + if l:import_path == -1 + call go#util#EchoError('could not determine package') + return + endif + let l:cmd = add(l:cmd, l:import_path) + else + let l:cmd = extend(l:cmd, a:000) + endif + + let l:type = 'errcheck' + if go#config#EchoCommandInfo() + call go#util#EchoProgress(printf('[%s] analyzing...', l:type)) + endif + let l:status = { + \ 'desc': 'current status', + \ 'type': l:type, + \ 'state': "started", + \ } redraw - let [l:out, l:err] = go#util#Exec([go#config#ErrcheckBin(), '-abspath', l:import_path]) + call go#statusline#Update(expand('%:p:h'), l:status) + + let [l:out, l:err] = go#util#ExecInDir(l:cmd) + + let l:status.state = 'success' + let l:state = 'PASS' let l:listtype = go#list#Type("GoErrCheck") if l:err != 0 - let l:winid = win_getid(winnr()) - let errformat = "%f:%l:%c:\ %m, %f:%l:%c\ %#%m" + let l:status.state = 'failed' + let l:state = 'FAIL' - " Parse and populate our location list - call go#list#ParseFormat(l:listtype, errformat, split(out, "\n"), 'Errcheck') + let l:winid = win_getid(winnr()) + + if l:err == 1 + let l:errformat = "%f:%l:%c:\ %m,%f:%l:%c\ %#%m" + " Parse and populate our location list + call go#list#ParseFormat(l:listtype, l:errformat, split(out, "\n"), 'Errcheck', 0) + endif let l:errors = go#list#Get(l:listtype) if empty(l:errors) @@ -195,18 +343,24 @@ function! go#lint#Errcheck(bang, ...) abort endif if !empty(errors) - call go#list#Populate(l:listtype, errors, 'Errcheck') - call go#list#Window(l:listtype, len(errors)) + call go#list#Populate(l:listtype, l:errors, 'Errcheck') + call go#list#Window(l:listtype, len(l:errors)) if !a:bang call go#list#JumpToFirst(l:listtype) else call win_gotoid(l:winid) endif endif + if go#config#EchoCommandInfo() + call go#util#EchoError(printf('[%s] %s', l:type, l:state)) + endif else call go#list#Clean(l:listtype) - call go#util#EchoSuccess('[errcheck] PASS') + if go#config#EchoCommandInfo() + call go#util#EchoSuccess(printf('[%s] %s', l:type, l:state)) + endif endif + call go#statusline#Update(expand('%:p:h'), l:status) endfunction function! go#lint#ToggleMetaLinterAutoSave() abort @@ -220,16 +374,19 @@ function! go#lint#ToggleMetaLinterAutoSave() abort call go#util#EchoProgress("auto metalinter enabled") endfunction -function! s:lint_job(args, bang, autosave) +function! s:lint_job(metalinter, args, bang, autosave) let l:opts = { \ 'statustype': a:args.statustype, \ 'errorformat': a:args.errformat, - \ 'for': "GoMetaLinter", + \ 'for': 'GoMetaLinter', \ 'bang': a:bang, - \ } + \ } if a:autosave - let l:opts.for = "GoMetaLinterAutoSave" + let l:opts.for = 'GoMetaLinterAutoSave' + " s:metalinterautosavecomplete is really only needed for golangci-lint + let l:opts.complete = funcref('s:metalinterautosavecomplete', [a:metalinter, expand('%:p:t')]) + let l:opts.preserveerrors = funcref('s:preserveerrors', [a:autosave]) endif " autowrite is not enabled for jobs @@ -238,45 +395,72 @@ function! s:lint_job(args, bang, autosave) call go#job#Spawn(a:args.cmd, l:opts) endfunction -function! s:metalintercmd(metalinter) +function! s:metalintercmd(metalinter, haslinter) let l:cmd = [] let bin_path = go#path#CheckBinPath(a:metalinter) if !empty(bin_path) - if a:metalinter == "gometalinter" - let l:cmd = s:gometalintercmd(bin_path) - elseif a:metalinter == "golangci-lint" - let l:cmd = s:golangcilintcmd(bin_path) + if a:metalinter == "golangci-lint" + let l:cmd = s:golangcilintcmd(bin_path, a:haslinter) endif endif return cmd endfunction -function! s:gometalintercmd(bin_path) - let cmd = [a:bin_path] - let cmd += ["--disable-all"] - - " gometalinter has a --tests flag to tell its linters whether to run - " against tests. While not all of its linters respect this flag, for those - " that do, it means if we don't pass --tests, the linter won't run against - " test files. One example of a linter that will not run against tests if - " we do not specify this flag is errcheck. - let cmd += ["--tests"] - return cmd -endfunction - -function! s:golangcilintcmd(bin_path) +function! s:golangcilintcmd(bin_path, haslinter) let cmd = [a:bin_path] let cmd += ["run"] let cmd += ["--print-issued-lines=false"] - let cmd += ["--disable-all"] + let cmd += ['--build-tags', go#config#BuildTags()] " do not use the default exclude patterns, because doing so causes golint " problems about missing doc strings to be ignored and other things that " golint identifies. let cmd += ["--exclude-use-default=false"] + + if a:haslinter + let cmd += ["--disable-all"] + endif + return cmd endfunction +function! s:metalinterautosavecomplete(metalinter, filepath, job, exit_code, messages) + if a:metalinter != 'golangci-lint' + return + endif + + if len(a:messages) == 0 + return + endif + + let l:idx = len(a:messages) - 1 + while l:idx >= 0 + " leave in any messages that report errors about a:filepath or that report + " more general problems that prevent golangci-lint from linting + " a:filepath. + if a:messages[l:idx] !~# '^' . a:filepath . ':' && a:messages[l:idx] !~# '^level=' + call remove(a:messages, l:idx) + endif + let l:idx -= 1 + endwhile +endfunction + +function! s:errorformat(metalinter) abort + if a:metalinter == 'golangci-lint' + " Golangci-lint can output the following: + " ::: () + " This can be defined by the following errorformat: + return 'level=%tarning\ msg="%m:\ [%f:%l:%c:\ %.%#]",level=%tarning\ msg="%m",level=%trror\ msg="%m:\ [%f:%l:%c:\ %.%#]",level=%trror\ msg="%m",%f:%l:%c:\ %m,%f:%l\ %m' + elseif a:metalinter == 'gopls' + return '%f:%l:%c:%t:\ %m,%f:%l:%c::\ %m,%f:%l::%t:\ %m' + endif + +endfunction + +function! s:preserveerrors(autosave, listtype) abort + return a:autosave && a:listtype == go#list#Type("GoFmt") && go#config#FmtAutosave() && isdirectory(expand('%:p:h')) +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/lint_test.vim b/pack/acp/start/vim-go/autoload/go/lint_test.vim index dccc138..6c556d3 100644 --- a/pack/acp/start/vim-go/autoload/go/lint_test.vim +++ b/pack/acp/start/vim-go/autoload/go/lint_test.vim @@ -2,23 +2,24 @@ let s:cpo_save = &cpo set cpo&vim -func! Test_Gometa() abort - call s:gometa('gometaliner') -endfunc - func! Test_GometaGolangciLint() abort call s:gometa('golangci-lint') endfunc func! s:gometa(metalinter) abort - let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnamemodify(getcwd(), ':p') . 'test-fixtures/lint') silent exe 'e ' . $GOPATH . '/src/lint/lint.go' try - let g:go_metalinter_comand = a:metalinter + let g:go_metalinter_command = a:metalinter let expected = [ \ {'lnum': 5, 'bufnr': bufnr('%')+1, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'pattern': '', 'text': 'exported function MissingFooDoc should have comment or be unexported (golint)'} \ ] + if a:metalinter == 'golangci-lint' + let expected = [ + \ {'lnum': 5, 'bufnr': bufnr('%')+5, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exported function `MissingFooDoc` should have comment or be unexported (golint)'} + \ ] + endif " clear the quickfix lists call setqflist([], 'r') @@ -36,34 +37,32 @@ func! s:gometa(metalinter) abort call gotest#assert_quickfix(actual, expected) finally + call call(RestoreGOPATH, []) unlet g:go_metalinter_enabled endtry endfunc -func! Test_GometaWithDisabled() abort - call s:gometawithdisabled('gometalinter') +func! Test_GometaGolangciLint_shadow() abort + call s:gometa_shadow('golangci-lint') endfunc -func! Test_GometaWithDisabledGolangciLint() abort - call s:gometawithdisabled('golangci-lint') -endfunc - -func! s:gometawithdisabled(metalinter) abort - let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' - silent exe 'e ' . $GOPATH . '/src/lint/lint.go' +func! s:gometa_shadow(metalinter) abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnamemodify(getcwd(), ':p') . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/lint/golangci-lint/problems/shadow/problems.go' try - let g:go_metalinter_comand = a:metalinter + let g:go_metalinter_command = a:metalinter let expected = [ - \ {'lnum': 5, 'bufnr': bufnr('%')+1, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'pattern': '', 'text': 'exported function MissingFooDoc should have comment or be unexported (golint)'} + \ {'lnum': 4, 'bufnr': bufnr('%'), 'col': 7, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'module': '', 'text': '[runner] Can''t run linter golint: golint: analysis skipped: errors in package'}, + \ {'lnum': 4, 'bufnr': bufnr('%'), 'col': 7, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'e', 'module': '', 'text': 'Running error: golint: analysis skipped: errors in package'} \ ] " clear the quickfix lists call setqflist([], 'r') - let g:go_metalinter_disabled = ['vet'] + let g:go_metalinter_enabled = ['golint'] - call go#lint#Gometa(0, 0, $GOPATH . '/src/foo') + call go#lint#Gometa(0, 0) let actual = getqflist() let start = reltime() @@ -74,58 +73,305 @@ func! s:gometawithdisabled(metalinter) abort call gotest#assert_quickfix(actual, expected) finally - unlet g:go_metalinter_disabled + call call(RestoreGOPATH, []) + unlet g:go_metalinter_enabled endtry endfunc -func! Test_GometaAutoSave() abort - call s:gometaautosave('gometalinter') -endfunc - func! Test_GometaAutoSaveGolangciLint() abort - call s:gometaautosave('golangci-lint') + call s:gometaautosave('golangci-lint', 0) endfunc -func! s:gometaautosave(metalinter) abort - let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' +func! Test_GometaAutoSaveKeepsErrors() abort + call s:gometaautosave('golangci-lint', 1) +endfunc + +func! s:gometaautosave(metalinter, withList) abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint') silent exe 'e ' . $GOPATH . '/src/lint/lint.go' try - let g:go_metalinter_comand = a:metalinter - let expected = [ + let g:go_metalinter_command = a:metalinter + let l:expected = [ \ {'lnum': 5, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'pattern': '', 'text': 'exported function MissingDoc should have comment or be unexported (golint)'} \ ] + if a:metalinter == 'golangci-lint' + let l:expected = [ + \ {'lnum': 5, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exported function `MissingDoc` should have comment or be unexported (golint)'} + \ ] + endif - let winnr = winnr() + let l:list = [] + if a:withList + let l:list = [ + \ {'lnum': 1, 'bufnr': 1, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'before metalinter'} + \ ] + let l:expected = extend(l:list, l:expected) + endif - " clear the location lists - call setloclist(l:winnr, [], 'r') + " set the location lists + call setloclist(0, l:list, 'r') let g:go_metalinter_autosave_enabled = ['golint'] call go#lint#Gometa(0, 1) - let actual = getloclist(l:winnr) + let l:actual = getloclist(0) + let l:start = reltime() + while len(l:actual) != len(l:expected) && reltimefloat(reltime(l:start)) < 10 + sleep 100m + let l:actual = getloclist(0) + endwhile + + call gotest#assert_quickfix(l:actual, l:expected) + finally + call call(RestoreGOPATH, []) + unlet g:go_metalinter_autosave_enabled + endtry +endfunc + +func! Test_GometaGolangciLint_importabs() abort + call s:gometa_importabs('golangci-lint') +endfunc + +func! s:gometa_importabs(metalinter) abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnamemodify(getcwd(), ':p') . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/lint/golangci-lint/problems/importabs/problems.go' + + try + let g:go_metalinter_command = a:metalinter + let expected = [ + \ {'lnum': 3, 'bufnr': bufnr('%'), 'col': 8, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'module': '', 'text': '[runner] Can''t run linter golint: golint: analysis skipped: errors in package'}, + \ {'lnum': 3, 'bufnr': bufnr('%'), 'col': 8, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'e', 'module': '', 'text': 'Running error: golint: analysis skipped: errors in package'}, + \ ] + " clear the quickfix lists + call setqflist([], 'r') + + let g:go_metalinter_enabled = ['golint'] + + call go#lint#Gometa(0, 0) + + let actual = getqflist() let start = reltime() while len(actual) == 0 && reltimefloat(reltime(start)) < 10 sleep 100m - let actual = getloclist(l:winnr) + let actual = getqflist() endwhile call gotest#assert_quickfix(actual, expected) finally + call call(RestoreGOPATH, []) + unlet g:go_metalinter_enabled + endtry +endfunc + +func! Test_GometaAutoSaveGolangciLint_importabs() abort + call s:gometaautosave_importabs('golangci-lint') +endfunc + +func! s:gometaautosave_importabs(metalinter) abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/lint/golangci-lint/problems/importabs/ok.go' + + try + let g:go_metalinter_command = a:metalinter + let expected = [ + \ {'lnum': 3, 'bufnr': bufnr('%')+1, 'col': 8, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'module': '', 'text': '[runner] Can''t run linter golint: golint: analysis skipped: errors in package'}, + \ {'lnum': 3, 'bufnr': bufnr('%')+1, 'col': 8, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'e', 'module': '', 'text': 'Running error: golint: analysis skipped: errors in package'} + \ ] + + " clear the location lists + call setloclist(0, [], 'r') + + let g:go_metalinter_autosave_enabled = ['golint'] + + call go#lint#Gometa(0, 1) + + let actual = getloclist(0) + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getloclist(0) + endwhile + + call gotest#assert_quickfix(actual, expected) + finally + call call(RestoreGOPATH, []) + unlet g:go_metalinter_autosave_enabled + endtry +endfunc + +func! Test_GometaGolangciLint_multiple() abort + call s:gometa_multiple('golangci-lint') +endfunc + +func! s:gometa_multiple(metalinter) abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnamemodify(getcwd(), ':p') . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/lint/golangci-lint/problems/multiple/problems.go' + + try + let g:go_metalinter_command = a:metalinter + let expected = [ + \ {'lnum': 8, 'bufnr': bufnr('%'), 'col': 7, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'module': '', 'text': '[runner] Can''t run linter golint: golint: analysis skipped: errors in package'}, + \ {'lnum': 8, 'bufnr': bufnr('%'), 'col': 7, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'e', 'module': '', 'text': 'Running error: golint: analysis skipped: errors in package'}, + \ ] + " clear the quickfix lists + call setqflist([], 'r') + + let g:go_metalinter_enabled = ['golint'] + + call go#lint#Gometa(0, 0) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + finally + call call(RestoreGOPATH, []) + unlet g:go_metalinter_enabled + endtry +endfunc + +func! Test_GometaAutoSaveGolangciLint_multiple() abort + call s:gometaautosave_multiple('golangci-lint') +endfunc + +func! s:gometaautosave_multiple(metalinter) abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/lint/golangci-lint/problems/multiple/problems.go' + + try + let g:go_metalinter_command = a:metalinter + let expected = [ + \ {'lnum': 8, 'bufnr': bufnr('%'), 'col': 7, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'w', 'module': '', 'text': '[runner] Can''t run linter golint: golint: analysis skipped: errors in package'}, + \ {'lnum': 8, 'bufnr': bufnr('%'), 'col': 7, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': 'e', 'module': '', 'text': 'Running error: golint: analysis skipped: errors in package'}, + \ ] + + " clear the location lists + call setloclist(0, [], 'r') + + let g:go_metalinter_autosave_enabled = ['golint'] + + call go#lint#Gometa(0, 1) + + let actual = getloclist(0) + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getloclist(0) + endwhile + + call gotest#assert_quickfix(actual, expected) + finally + call call(RestoreGOPATH, []) unlet g:go_metalinter_autosave_enabled endtry endfunc func! Test_Vet() abort - let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint' - silent exe 'e ' . $GOPATH . '/src/vet/vet.go' + let l:tmp = gotest#load_fixture('lint/src/vet/vet.go') + + try + let expected = [ + \ {'lnum': 7, 'bufnr': bufnr('%'), 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', + \ 'text': 'Printf format %d has arg str of wrong type string'} + \ ] + + let winnr = winnr() + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Vet(1) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! Test_Vet_subdir() abort + let l:tmp = gotest#load_fixture('lint/src/vet/vet.go') + + " go up one directory to easily test that go vet's file paths are handled + " correctly when the working directory is not the directory that contains + " the file being vetted. + call go#util#Chdir('..') + + try + let expected = [ + \ {'lnum': 7, 'bufnr': bufnr('%'), 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', + \ 'text': 'Printf format %d has arg str of wrong type string'} + \ ] + + let winnr = winnr() + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Vet(1) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! Test_Vet_compilererror() abort + let l:tmp = gotest#load_fixture('lint/src/vet/compilererror/compilererror.go') + + try + let expected = [ + \ {'lnum': 6, 'bufnr': bufnr('%'), 'col': 22, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': "missing ',' before newline in argument list (and 1 more errors)"} + \ ] + + let winnr = winnr() + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Vet(1) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! Test_Lint_GOPATH() abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint') + + silent exe 'e ' . $GOPATH . '/src/lint/lint.go' compiler go let expected = [ - \ {'lnum': 7, 'bufnr': bufnr('%'), 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', - \ 'text': 'Printf format %d has arg str of wrong type string'} + \ {'lnum': 5, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exported function MissingDoc should have comment or be unexported'}, + \ {'lnum': 5, 'bufnr': bufnr('%')+7, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exported function AlsoMissingDoc should have comment or be unexported'} \ ] let winnr = winnr() @@ -133,7 +379,35 @@ func! Test_Vet() abort " clear the location lists call setqflist([], 'r') - call go#lint#Vet(1) + call go#lint#Golint(1) + + let actual = getqflist() + let start = reltime() + while len(actual) == 0 && reltimefloat(reltime(start)) < 10 + sleep 100m + let actual = getqflist() + endwhile + + call gotest#assert_quickfix(actual, expected) + + call call(RestoreGOPATH, []) +endfunc + +func! Test_Lint_NullModule() abort + silent exe 'e ' . fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/lint/src/lint/lint.go' + compiler go + + let expected = [ + \ {'lnum': 5, 'bufnr': bufnr('%'), 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exported function MissingDoc should have comment or be unexported'}, + \ {'lnum': 5, 'bufnr': bufnr('%')+7, 'col': 1, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'exported function AlsoMissingDoc should have comment or be unexported'} + \ ] + + let winnr = winnr() + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Golint(1) let actual = getqflist() let start = reltime() @@ -145,6 +419,70 @@ func! Test_Vet() abort call gotest#assert_quickfix(actual, expected) endfunc +func! Test_Errcheck() abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnamemodify(getcwd(), ':p') . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/errcheck/errcheck.go' + + try + let l:bufnr = bufnr('') + let expected = [ + \ {'lnum': 9, 'bufnr': bufnr(''), 'col': 9, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'module': '', 'text': ":\tio.Copy(os.Stdout, os.Stdin)"}, + \ {'lnum': 10, 'bufnr': bufnr('')+1, 'col': 9, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'module': '', 'text': ":\tio.Copy(os.Stdout, os.Stdin)"}, + \ ] + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Errcheck(1) + + call gotest#assert_quickfix(getqflist(), expected) + call assert_equal(l:bufnr, bufnr('')) + finally + call call(RestoreGOPATH, []) + endtry +endfunc + +func! Test_Errcheck_options() abort + let RestoreGOPATH = go#util#SetEnv('GOPATH', fnamemodify(getcwd(), ':p') . 'test-fixtures/lint') + silent exe 'e ' . $GOPATH . '/src/errcheck/errcheck.go' + + try + let l:bufnr = bufnr('') + let expected = [ + \ {'lnum': 9, 'bufnr': bufnr(''), 'col': 9, 'pattern': '', 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'module': '', 'text': ":\tio.Copy(os.Stdout, os.Stdin)"}, + \ ] + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Errcheck(1, '-ignoretests') + + call gotest#assert_quickfix(getqflist(), expected) + call assert_equal(l:bufnr, bufnr('')) + finally + call call(RestoreGOPATH, []) + endtry +endfunc + +func! Test_Errcheck_compilererror() abort + let l:tmp = gotest#load_fixture('lint/src/errcheck/compilererror/compilererror.go') + + try + let l:bufnr = bufnr('') + let expected = [] + + " clear the location lists + call setqflist([], 'r') + + call go#lint#Errcheck(1) + + call gotest#assert_quickfix(getqflist(), expected) + call assert_equal(l:bufnr, bufnr('')) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/list.vim b/pack/acp/start/vim-go/autoload/go/list.vim index 1805d4d..e700203 100644 --- a/pack/acp/start/vim-go/autoload/go/list.vim +++ b/pack/acp/start/vim-go/autoload/go/list.vim @@ -49,26 +49,23 @@ endfunction function! go#list#Populate(listtype, items, title) abort if a:listtype == "locationlist" call setloclist(0, a:items, 'r') - - " The last argument ({what}) is introduced with 7.4.2200: - " https://github.com/vim/vim/commit/d823fa910cca43fec3c31c030ee908a14c272640 - if has("patch-7.4.2200") | call setloclist(0, [], 'a', {'title': a:title}) | endif + call setloclist(0, [], 'a', {'title': a:title}) else call setqflist(a:items, 'r') - if has("patch-7.4.2200") | call setqflist([], 'a', {'title': a:title}) | endif + call setqflist([], 'a', {'title': a:title}) endif endfunction " Parse parses the given items based on the specified errorformat and " populates the list. -function! go#list#ParseFormat(listtype, errformat, items, title) abort +function! go#list#ParseFormat(listtype, errformat, items, title, add) abort " backup users errorformat, will be restored once we are finished let old_errorformat = &errorformat " parse and populate the location list let &errorformat = a:errformat try - call go#list#Parse(a:listtype, a:items, a:title) + call go#list#Parse(a:listtype, a:items, a:title, a:add) finally "restore back let &errorformat = old_errorformat @@ -77,13 +74,26 @@ endfunction " Parse parses the given items based on the global errorformat and " populates the list. -function! go#list#Parse(listtype, items, title) abort +function! go#list#Parse(listtype, items, title, add) abort + let l:list = [] + if a:add + let l:list = go#list#Get(a:listtype) + endif + if a:listtype == "locationlist" - lgetexpr a:items - if has("patch-7.4.2200") | call setloclist(0, [], 'a', {'title': a:title}) | endif + if a:add + laddexpr a:items + else + lgetexpr a:items + endif + call setloclist(0, [], 'a', {'title': a:title}) else - cgetexpr a:items - if has("patch-7.4.2200") | call setqflist([], 'a', {'title': a:title}) | endif + if a:add + caddexpr a:items + else + cgetexpr a:items + endif + call setqflist([], 'a', {'title': a:title}) endif endfunction @@ -138,6 +148,7 @@ endfunction " in g:go_list_type_commands. let s:default_list_type_commands = { \ "GoBuild": "quickfix", + \ "GoDiagnostics": "quickfix", \ "GoDebug": "quickfix", \ "GoErrCheck": "quickfix", \ "GoFmt": "locationlist", @@ -152,6 +163,8 @@ let s:default_list_type_commands = { \ "GoRun": "quickfix", \ "GoTest": "quickfix", \ "GoVet": "quickfix", + \ "GoReferrers": "locationlist", + \ "GoImplements": "locationlist", \ "_guru": "locationlist", \ "_term": "locationlist", \ "_job": "locationlist", diff --git a/pack/acp/start/vim-go/autoload/go/lsp.vim b/pack/acp/start/vim-go/autoload/go/lsp.vim index aa21bce..6b08acf 100644 --- a/pack/acp/start/vim-go/autoload/go/lsp.vim +++ b/pack/acp/start/vim-go/autoload/go/lsp.vim @@ -7,8 +7,7 @@ scriptencoding utf-8 let s:lspfactory = {} function! s:lspfactory.get() dict abort - if !has_key(self, 'current') - " TODO(bc): check that the lsp is still running. + if empty(get(self, 'current', {})) || empty(get(self.current, 'job', {})) let self.current = s:newlsp() endif @@ -22,12 +21,6 @@ function! s:lspfactory.reset() dict abort endfunction function! s:newlsp() abort - if !go#util#has_job() - " TODO(bc): start the server in the background using a shell that waits for the right output before returning. - call go#util#EchoError('This feature requires either Vim 8.0.0087 or newer with +job or Neovim.') - return - endif - " job is the job used to talk to the backing instance of gopls. " ready is 0 until the initialize response has been received. 1 afterwards. " queue is messages to send after initialization @@ -41,6 +34,18 @@ function! s:newlsp() abort " * handleResult takes a single argument, the result message received from gopls " * error takes a single argument, the error message received from gopls. " The error method is optional. + " workspaceDirectories is an array of named workspaces. + " wd is the working directory for gopls + " diagnostics is a dictionary whose keys are filenames and each value is a + " list of diagnostic messages for the file. + " diagnosticsQueue is a queue of diagnostics notifications that have been + " received, but not yet processed. + " fileVersions is a dictionary of filenames to versions. + " notificationQueue is a dictionary of filenames to functions. For a given + " filename, each notification will call the first function in the list of + " function values and remove it from the list. The functions should accept + " two arguments: an absolute path and a list of diagnotics messages for + " the file. let l:lsp = { \ 'job': '', \ 'ready': 0, @@ -48,8 +53,32 @@ function! s:newlsp() abort \ 'last_request_id': 0, \ 'buf': '', \ 'handlers': {}, + \ 'workspaceDirectories': [], + \ 'wd' : '', + \ 'diagnosticsQueue': [], + \ 'diagnostics': {}, + \ 'fileVersions': {}, + \ 'notificationQueue': {}, \ } + if !go#config#GoplsEnabled() + let l:lsp.sendMessage = funcref('s:noop') + return l:lsp + endif + + if !go#util#has_job() + let l:oldshortmess=&shortmess + if has('nvim') + set shortmess-=F + endif + call go#util#EchoWarning('Features that rely on gopls will not work without either Vim 8.0.0087 or newer with +job or Neovim') + " Sleep one second to make sure people see the message. Otherwise it is + " often immediately overwritten by an async message. + sleep 1 + let &shortmess=l:oldshortmess + return l:lsp + endif + function! l:lsp.readMessage(data) dict abort let l:responses = [] let l:rest = a:data @@ -76,19 +105,17 @@ function! s:newlsp() abort endif " get the start of the rest - let l:rest_start_idx = l:body_start_idx + str2nr(l:length_match[1]) + let l:next_start_idx = l:body_start_idx + str2nr(l:length_match[1]) - if len(l:rest) < l:rest_start_idx + if len(l:rest) < l:next_start_idx " incomplete response body break endif - if go#util#HasDebug('lsp') - let g:go_lsp_log = add(go#config#LspLog(), "<-\n" . l:rest[:l:rest_start_idx - 1]) - endif + call s:debug('received', l:rest[:l:next_start_idx - 1]) - let l:body = l:rest[l:body_start_idx : l:rest_start_idx - 1] - let l:rest = l:rest[l:rest_start_idx :] + let l:body = l:rest[l:body_start_idx : l:next_start_idx - 1] + let l:rest = l:rest[l:next_start_idx :] try " add the json body to the list. @@ -106,36 +133,176 @@ function! s:newlsp() abort function! l:lsp.handleMessage(ch, data) dict abort let self.buf .= a:data - let [self.buf, l:responses] = self.readMessage(self.buf) + let [self.buf, l:messages] = self.readMessage(self.buf) - " TODO(bc): handle notifications (e.g. window/showMessage). - - for l:response in l:responses - if has_key(l:response, 'id') && has_key(self.handlers, l:response.id) - try - let l:handler = self.handlers[l:response.id] - - if has_key(l:response, 'error') - call l:handler.requestComplete(0) - if has_key(l:handler, 'error') - call call(l:handler.error, [l:response.error.message]) - else - call go#util#EchoError(l:response.error.message) - endif - return - endif - call l:handler.requestComplete(1) - call call(l:handler.handleResult, [l:response.result]) - finally - call remove(self.handlers, l:response.id) - endtry + for l:message in l:messages + if has_key(l:message, 'method') + if has_key(l:message, 'id') + call self.handleRequest(l:message) + else + call self.handleNotification(l:message) + endif + elseif has_key(l:message, 'result') || has_key(l:message, 'error') + call self.handleResponse(l:message) endif endfor endfunction + function! l:lsp.handleRequest(req) dict abort + if a:req.method == 'workspace/workspaceFolders' + let l:resp = go#lsp#message#WorkspaceFoldersResult(self.workspaceDirectories) + elseif a:req.method == 'workspace/configuration' && has_key(a:req, 'params') && has_key(a:req.params, 'items') + let l:resp = go#lsp#message#ConfigurationResult(a:req.params.items) + elseif a:req.method == 'client/registerCapability' && has_key(a:req, 'params') && has_key(a:req.params, 'registrations') + let l:resp = v:null + else + return + endif + + if get(self, 'exited', 0) + return + endif + + let l:msg = self.newResponse(a:req.id, l:resp) + call self.write(l:msg) + endfunction + + function! l:lsp.handleResponse(resp) dict abort + if has_key(a:resp, 'id') && has_key(self.handlers, a:resp.id) + try + let l:handler = self.handlers[a:resp.id] + + let l:winid = win_getid(winnr()) + " Always set the active window to the window that was active when + " the request was sent. Among other things, this makes sure that + " the correct window's location list will be populated when the + " list type is 'location' and the user has moved windows since + " sending the request. + call win_gotoid(l:handler.winid) + + if has_key(a:resp, 'error') + call l:handler.requestComplete(0) + if has_key(l:handler, 'error') + call call(l:handler.error, [a:resp.error.message]) + else + call go#util#EchoError(a:resp.error.message) + endif + call win_gotoid(l:winid) + return + endif + call l:handler.requestComplete(1) + + let l:winidBeforeHandler = l:handler.winid + call call(l:handler.handleResult, [a:resp.result]) + + " change the window back to the window that was active when + " starting to handle the message _only_ if the handler didn't + " update the winid, so that handlers can set the winid if needed + " (e.g. :GoDef). + if l:handler.winid == l:winidBeforeHandler + call win_gotoid(l:winid) + endif + finally + call remove(self.handlers, a:resp.id) + endtry + endif + endfunction + + function! l:lsp.handleNotification(req) dict abort + " TODO(bc): handle more notifications (e.g. window/showMessage). + if a:req.method == 'textDocument/publishDiagnostics' + call self.handleDiagnostics(a:req.params) + endif + endfunction + + function! l:lsp.handleDiagnostics(data) dict abort + let self.diagnosticsQueue = add(self.diagnosticsQueue, a:data) + call self.updateDiagnostics() + endfunction + + " TODO(bc): process the queue asynchronously + function! l:lsp.updateDiagnostics() dict abort + for l:data in self.diagnosticsQueue + call remove(self.diagnosticsQueue, 0) + + try + let l:diagnostics = [] + let l:errorMatches = [] + let l:warningMatches = [] + let l:fname = go#path#FromURI(l:data.uri) + + " get the buffer name relative to the current directory, because + " Vim says that a buffer name can't be an absolute path. + let l:bufname = fnamemodify(l:fname, ':.') + + if len(l:data.diagnostics) > 0 && (go#config#DiagnosticsEnabled() || bufnr(l:bufname) == bufnr('')) + " make sure the buffer is listed and loaded before calling getbufline() on it + if !bufexists(l:bufname) + "let l:starttime = reltime() + call bufadd(l:bufname) + endif + + if !bufloaded(l:bufname) + "let l:starttime = reltime() + call bufload(l:bufname) + endif + + for l:diag in l:data.diagnostics + " TODO(bc): cache the raw diagnostics when they're not for the + " current buffer so that they can be processed when it is the + " current buffer and highlight the areas of concern. + let [l:error, l:matchpos] = s:errorFromDiagnostic(l:diag, l:bufname, l:fname) + let l:diagnostics = add(l:diagnostics, l:error) + + if empty(l:matchpos) + continue + endif + + if l:diag.severity == 1 + let l:errorMatches = add(l:errorMatches, l:matchpos) + elseif l:diag.severity == 2 + let l:warningMatches = add(l:warningMatches, l:matchpos) + endif + endfor + endif + + if bufnr(l:bufname) == bufnr('') + " only apply highlighting when the diagnostics are for the current + " version. + let l:lsp = s:lspfactory.get() + let l:version = get(l:lsp.fileVersions, l:fname, 0) + " it's tempting to only highlight matches when they are for the + " current version of the buffer, but that causes problems when the + " version number has been updated and the content has not. In such a + " case, the diagnostics may not be sent for later versions. + call s:highlightMatches(l:errorMatches, l:warningMatches) + endif + + let self.diagnostics[l:fname] = l:diagnostics + if has_key(self.notificationQueue, l:fname) && len(self.notificationQueue[l:fname]) > 0 + call call(self.notificationQueue[l:fname][0], copy(l:diagnostics)) + call remove(self.notificationQueue[l:fname], 0) + endif + catch + call go#util#EchoError(printf('%s: %s', v:throwpoint, v:exception)) + endtry + endfor + endfunction + function! l:lsp.handleInitializeResult(result) dict abort + if go#config#EchoCommandInfo() + call go#util#EchoProgress("initialized gopls") + endif + let status = { + \ 'desc': '', + \ 'type': 'gopls', + \ 'state': 'initialized', + \ } + call go#statusline#Update(self.wd, status) + let self.ready = 1 - " TODO(bc): send initialized message to the server? + let l:msg = self.newMessage(go#lsp#message#Initialized()) + call self.write(l:msg) " send messages queued while waiting for ready. for l:item in self.queue @@ -148,12 +315,34 @@ function! s:newlsp() abort function! l:lsp.sendMessage(data, handler) dict abort if !self.last_request_id - " TODO(bc): run a server per module and one per GOPATH? (may need to - " keep track of servers by rootUri). - let l:msg = self.newMessage(go#lsp#message#Initialize(getcwd())) + let l:wd = go#util#ModuleRoot() + if l:wd == -1 + call go#util#EchoError('could not determine appropriate working directory for gopls') + return -1 + endif + + if l:wd == '' + let l:wd = getcwd() + endif + let self.wd = l:wd + + if go#config#EchoCommandInfo() + call go#util#EchoProgress("initializing gopls") + endif + + let l:status = { + \ 'desc': '', + \ 'type': 'gopls', + \ 'state': 'initializing', + \ } + call go#statusline#Update(l:wd, l:status) + + let self.workspaceDirectories = add(self.workspaceDirectories, l:wd) + let l:msg = self.newMessage(go#lsp#message#Initialize(l:wd)) let l:state = s:newHandlerState('') let l:state.handleResult = funcref('self.handleInitializeResult', [], l:self) + let self.handlers[l:msg.id] = l:state call l:state.start() @@ -180,7 +369,7 @@ function! s:newlsp() abort let l:msg = { \ 'method': a:data.method, \ 'jsonrpc': '2.0', - \ } + \ } if !a:data.notification let self.last_request_id += 1 @@ -194,14 +383,26 @@ function! s:newlsp() abort return l:msg endfunction - function! l:lsp.write(msg) dict abort - let l:body = json_encode(a:msg) - let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body + function l:lsp.newResponse(id, result) dict abort + let l:msg = { + \ 'jsonrpc': '2.0', + \ 'id': a:id, + \ 'result': a:result, + \ } - if go#util#HasDebug('lsp') - let g:go_lsp_log = add(go#config#LspLog(), "->\n" . l:data) + return l:msg + endfunction + + function! l:lsp.write(msg) dict abort + if empty(get(self, 'job', {})) + return endif + let l:body = json_encode(a:msg) + let l:data = 'Content-Length: ' . strlen(l:body) . "\r\n\r\n" . l:body + + call s:debug('sent', l:data) + if has('nvim') call chansend(self.job, l:data) return @@ -210,19 +411,39 @@ function! s:newlsp() abort call ch_sendraw(self.job, l:data) endfunction - function! l:lsp.exit_cb(job, exit_status) dict abort + function! l:lsp.exit_cb(job, exit_status) dict + let self.exited = 1 + if !get(self, 'restarting', 0) + return + endif + + let l:queue = self.queue + + let l:workspaces = self.workspaceDirectories + call s:lspfactory.reset() + let l:lsp = s:lspfactory.get() + + " restore workspaces + call call('go#lsp#AddWorkspaceDirectory', l:workspaces) + " * send DidOpen messages for all buffers that have b:did_lsp_open set + " TODO(bc): check modifiable and filetype, too? + bufdo if get(b:, 'go_lsp_did_open', 0) | if &modified | call go#lsp#DidOpen(expand('%:p')) | else | call go#lsp#DidChange(expand('%:p')) | endif | endif + let l:lsp.queue = extend(l:lsp.queue, l:queue) + return endfunction - " explicitly bind close_cb to state so that within it, self will always refer function! l:lsp.close_cb(ch) dict abort - " TODO(bc): does anything need to be done here? + " TODO(bc): remove the buffer variables that indicate that gopls has been + " informed that the file is open endfunction function! l:lsp.err_cb(ch, msg) dict abort - if go#util#HasDebug('lsp') - let g:go_lsp_log = add(go#config#LspLog(), "<-stderr\n" . a:msg) + if a:msg =~ '^\d\{4}/\d\d/\d\d\ \d\d:\d\d:\d\d debug server listening on port \d\+$' && !get(self, 'debugport', 0) + let self.debugport = substitute(a:msg, '\d\{4}/\d\d/\d\d\ \d\d:\d\d:\d\d debug server listening on port \(\d\+\).*$', '\1', '') endif + + call s:debug('stderr', a:msg) endfunction " explicitly bind callbacks to l:lsp so that within it, self will always refer @@ -241,14 +462,31 @@ function! s:newlsp() abort let l:bin_path = go#path#CheckBinPath("gopls") if empty(l:bin_path) - return + return l:lsp endif - " TODO(bc): output a message indicating which directory lsp is going to - " start in. - let l:lsp.job = go#job#Start([l:bin_path], l:opts) + let l:cmd = [l:bin_path] + let l:cmdopts = go#config#GoplsOptions() + + if go#util#HasDebug('lsp') + " debugging can be enabled either with g:go_debug or with + " g:go_gopls_options; use g:go_gopls_options if it's given in case users + " are running the gopls debug server on a known port. + let l:needsDebug = 1 + + for l:item in l:cmdopts + let l:idx = stridx(l:item, '-debug') + if l:idx == 0 || l:idx == 1 + let l:needsDebug = 0 + endif + endfor + if l:needsDebug + let l:cmd = extend(l:cmd, ['-debug', 'localhost:0']) + endif + endif + + let l:lsp.job = go#job#Start(l:cmd+l:cmdopts, l:opts) - " TODO(bc): send the initialize message now? return l:lsp endfunction @@ -260,6 +498,7 @@ function! s:newHandlerState(statustype) abort \ 'winid': win_getid(winnr()), \ 'statustype': a:statustype, \ 'jobdir': getcwd(), + \ 'handleResult': funcref('s:noop'), \ } " explicitly bind requestComplete to state so that within it, self will @@ -308,16 +547,17 @@ function! s:requestComplete(ok) abort dict endfunction function! s:start() abort dict - if self.statustype != '' - let status = { - \ 'desc': 'current status', - \ 'type': self.statustype, - \ 'state': "started", - \ } - - call go#statusline#Update(self.jobdir, status) - endif let self.started_at = reltime() + if self.statustype == '' + return + endif + let status = { + \ 'desc': 'current status', + \ 'type': self.statustype, + \ 'state': "started", + \ } + + call go#statusline#Update(self.jobdir, status) endfunction " go#lsp#Definition calls gopls to get the definition of the identifier at @@ -332,13 +572,17 @@ function! go#lsp#Definition(fname, line, col, handler) abort let l:state = s:newHandlerState('definition') let l:state.handleResult = funcref('s:definitionHandler', [function(a:handler, [], l:state)], l:state) let l:msg = go#lsp#message#Definition(fnamemodify(a:fname, ':p'), a:line, a:col) - call l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:definitionHandler(next, msg) abort dict + if a:msg is v:null || len(a:msg) == 0 + return + endif + " gopls returns a []Location; just take the first one. let l:msg = a:msg[0] - let l:args = [[printf('%s:%d:%d: %s', go#path#FromURI(l:msg.uri), l:msg.range.start.line+1, l:msg.range.start.character+1, 'lsp does not supply a description')]] + let l:args = [[printf('%s:%d:%d: %s', go#path#FromURI(l:msg.uri), l:msg.range.start.line+1, go#lsp#lsp#PositionOf(getline(l:msg.range.start.line+1), l:msg.range.start.character), 'lsp does not supply a description')]] call call(a:next, l:args) endfunction @@ -354,13 +598,17 @@ function! go#lsp#TypeDef(fname, line, col, handler) abort let l:state = s:newHandlerState('type definition') let l:msg = go#lsp#message#TypeDefinition(fnamemodify(a:fname, ':p'), a:line, a:col) let l:state.handleResult = funcref('s:typeDefinitionHandler', [function(a:handler, [], l:state)], l:state) - call l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:typeDefinitionHandler(next, msg) abort dict + if a:msg is v:null || len(a:msg) == 0 + return + endif + " gopls returns a []Location; just take the first one. let l:msg = a:msg[0] - let l:args = [[printf('%s:%d:%d: %s', go#path#FromURI(l:msg.uri), l:msg.range.start.line+1, l:msg.range.start.character+1, 'lsp does not supply a description')]] + let l:args = [[printf('%s:%d:%d: %s', go#path#FromURI(l:msg.uri), l:msg.range.start.line+1, go#lsp#lsp#PositionOf(getline(l:msg.range.start.line+1), l:msg.range.start.character), 'lsp does not supply a description')]] call call(a:next, l:args) endfunction @@ -369,17 +617,28 @@ function! go#lsp#DidOpen(fname) abort return endif - if !filereadable(a:fname) + let l:fname = fnamemodify(a:fname, ':p') + if !isdirectory(fnamemodify(l:fname, ':h')) return endif let l:lsp = s:lspfactory.get() - let l:msg = go#lsp#message#DidOpen(fnamemodify(a:fname, ':p'), join(go#util#GetLines(), "\n") . "\n") - let l:state = s:newHandlerState('') - let l:state.handleResult = funcref('s:noop') - call l:lsp.sendMessage(l:msg, l:state) + if !has_key(l:lsp.notificationQueue, l:fname) + let l:lsp.notificationQueue[l:fname] = [] + endif + + let l:lsp.fileVersions[l:fname] = getbufvar(l:fname, 'changedtick') + + let l:msg = go#lsp#message#DidOpen(l:fname, join(go#util#GetLines(), "\n") . "\n", l:lsp.fileVersions[l:fname]) + let l:state = s:newHandlerState('') + + " TODO(bc): setting a buffer level variable here assumes that a:fname is the + " current buffer. Change to a:fname first before setting it and then change + " back to active buffer. let b:go_lsp_did_open = 1 + + return l:lsp.sendMessage(l:msg, l:state) endfunction function! go#lsp#DidChange(fname) abort @@ -390,21 +649,29 @@ function! go#lsp#DidChange(fname) abort return endif - call go#lsp#DidOpen(a:fname) - - if !filereadable(a:fname) + let l:fname = fnamemodify(a:fname, ':p') + if !isdirectory(fnamemodify(l:fname, ':h')) return endif + call go#lsp#DidOpen(a:fname) + let l:lsp = s:lspfactory.get() - let l:msg = go#lsp#message#DidChange(fnamemodify(a:fname, ':p'), join(go#util#GetLines(), "\n") . "\n") + + let l:version = getbufvar(l:fname, 'changedtick') + if has_key(l:lsp.fileVersions, l:fname) && l:lsp.fileVersions[l:fname] == l:version + return + endif + let l:lsp.fileVersions[l:fname] = l:version + + let l:msg = go#lsp#message#DidChange(l:fname, join(go#util#GetLines(), "\n") . "\n", l:lsp.fileVersions[l:fname]) let l:state = s:newHandlerState('') - let l:state.handleResult = funcref('s:noop') - call l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction function! go#lsp#DidClose(fname) abort - if !filereadable(a:fname) + let l:fname = fnamemodify(a:fname, ':p') + if !isdirectory(fnamemodify(l:fname, ':h')) return endif @@ -413,12 +680,14 @@ function! go#lsp#DidClose(fname) abort endif let l:lsp = s:lspfactory.get() - let l:msg = go#lsp#message#DidClose(fnamemodify(a:fname, ':p')) + let l:msg = go#lsp#message#DidClose(l:fname) let l:state = s:newHandlerState('') - let l:state.handleResult = funcref('s:noop') - call l:lsp.sendMessage(l:msg, l:state) - + " TODO(bc): setting a buffer level variable here assumes that a:fname is the + " current buffer. Change to a:fname first before setting it and then change + " back to active buffer. let b:go_lsp_did_open = 0 + + return l:lsp.sendMessage(l:msg, l:state) endfunction function! go#lsp#Completion(fname, line, col, handler) abort @@ -429,30 +698,199 @@ function! go#lsp#Completion(fname, line, col, handler) abort let l:state = s:newHandlerState('completion') let l:state.handleResult = funcref('s:completionHandler', [function(a:handler, [], l:state)], l:state) let l:state.error = funcref('s:completionErrorHandler', [function(a:handler, [], l:state)], l:state) - call l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:completionHandler(next, msg) abort dict " gopls returns a CompletionList. let l:matches = [] + let l:start = -1 + for l:item in a:msg.items - let l:match = {'abbr': l:item.label, 'word': l:item.textEdit.newText, 'info': '', 'kind': go#lsp#completionitemkind#Vim(l:item.kind)} + let l:start = l:item.textEdit.range.start.character + + let l:match = {'abbr': l:item.label, 'word': l:item.textEdit.newText, 'info': '', 'kind': go#lsp#completionitemkind#Vim(l:item.kind), 'user_data': '', 'icase': go#config#CodeCompletionIcase()} if has_key(l:item, 'detail') - let l:item.info = l:item.detail + let l:match.menu = l:item.detail + if go#lsp#completionitemkind#IsFunction(l:item.kind) || go#lsp#completionitemkind#IsMethod(l:item.kind) + let l:match.info = printf('%s %s', l:item.label, l:item.detail) + + " The detail provided by gopls hasn't always provided the the full + " signature including the return value. The label used to be the + " function signature and the detail was the return value. Handle + " that case for backward compatibility. This can be removed in the + " future once it's likely that the majority of users are on a recent + " version of gopls. + if l:item.detail !~ '^func' + let l:match.info = printf('func %s %s', l:item.label, l:item.detail) + endif + endif endif + let l:match.user_data = l:match.info if has_key(l:item, 'documentation') let l:match.info .= "\n\n" . l:item.documentation endif let l:matches = add(l:matches, l:match) endfor - let l:args = [l:matches] + let l:args = [l:start, l:matches] call call(a:next, l:args) endfunction function! s:completionErrorHandler(next, error) abort dict - call call(a:next, [[]]) + call call(a:next, [-1, []]) +endfunction + +" go#lsp#SameIDs calls gopls to get the references to the identifier at line +" and col in fname. handler should be a dictionary function that takes a list +" of strings in the form 'file:line:col: message'. handler will be attached to +" a dictionary that manages state (statuslines, sets the winid, etc.). handler +" should take three arguments: an exit_code, a JSON object encoded to a string +" that mimics guru's ouput for `what`, and third mode parameter that only +" exists for compatibility with the guru implementation of SameIDs. +" TODO(bc): refactor to not need the guru adapter. +function! go#lsp#SameIDs(showstatus, fname, line, col, handler) abort + call go#lsp#DidChange(a:fname) + + let l:lsp = s:lspfactory.get() + let l:msg = go#lsp#message#References(a:fname, a:line, a:col) + + if a:showstatus + let l:state = s:newHandlerState('same ids') + else + let l:state = s:newHandlerState('') + endif + + let l:state.handleResult = funcref('s:sameIDsHandler', [function(a:handler, [], l:state)], l:state) + let l:state.error = funcref('s:noop') + return l:lsp.sendMessage(l:msg, l:state) +endfunction + +function! s:sameIDsHandler(next, msg) abort dict + let l:furi = go#path#ToURI(expand('%:p')) + + let l:result = { + \ 'sameids': [], + \ 'enclosing': [], + \ } + + let l:msg = a:msg + if a:msg is v:null + let l:msg = [] + endif + + for l:loc in l:msg + if l:loc.uri !=# l:furi + continue + endif + + if len(l:result.enclosing) == 0 + let l:result.enclosing = [{ + \ 'desc': 'identifier', + \ 'start': l:loc.range.start.character+1, + \ 'end': l:loc.range.end.character+1, + \ }] + endif + + let l:result.sameids = add(l:result.sameids, printf('%s:%s:%s', go#path#FromURI(l:loc.uri), l:loc.range.start.line+1, l:loc.range.start.character+1)) + endfor + + call call(a:next, [0, json_encode(l:result), '']) +endfunction + +" go#lsp#Referrers calls gopls to get the references to the identifier at line +" and col in fname. handler should be a dictionary function that takes a list +" of strings in the form 'file:line:col: message'. handler will be attached to +" a dictionary that manages state (statuslines, sets the winid, etc.). handler +" should take three arguments: an exit_code, a JSON object encoded to a string +" that mimics guru's ouput for `what`, and third mode parameter that only +" exists for compatibility with the guru implementation of SameIDs. +" TODO(bc): refactor to not need the guru adapter. +function! go#lsp#Referrers(fname, line, col, handler) abort + call go#lsp#DidChange(a:fname) + + let l:lsp = s:lspfactory.get() + let l:msg = go#lsp#message#References(a:fname, a:line, a:col) + + let l:state = s:newHandlerState('referrers') + + let l:state.handleResult = funcref('s:handleReferences', [function(a:handler, [], l:state)], l:state) + let l:state.error = funcref('s:noop') + return l:lsp.sendMessage(l:msg, l:state) +endfunction + +function! s:handleReferences(next, msg) abort dict + call s:handleLocations(a:next, a:msg) +endfunction + +function! s:handleLocations(next, msg) abort + let l:result = [] + + let l:msg = a:msg + + if l:msg is v:null + let l:msg = [] + endif + + call sort(l:msg, funcref('s:compareLocations')) + + for l:loc in l:msg + let l:fname = go#path#FromURI(l:loc.uri) + let l:line = l:loc.range.start.line+1 + let l:bufnr = bufnr(l:fname) + let l:bufinfo = getbufinfo(l:fname) + + try + if l:bufnr == -1 || len(l:bufinfo) == 0 || l:bufinfo[0].loaded == 0 + let l:filecontents = readfile(l:fname, '', l:line) + else + let l:filecontents = getbufline(l:fname, l:line) + endif + + if len(l:filecontents) == 0 + continue + endif + + let l:content = l:filecontents[-1] + catch + call go#util#EchoError(printf('%s (line %s): %s at %s', l:fname, l:line, v:exception, v:throwpoint)) + endtry + + let l:item = printf('%s:%s:%s: %s', go#path#FromURI(l:loc.uri), l:line, go#lsp#lsp#PositionOf(l:content, l:loc.range.start.character), l:content) + + let l:result = add(l:result, l:item) + endfor + + call call(a:next, [0, l:result, '']) +endfunction + +" go#lsp#Implementations calls gopls to get the implementations to the +" identifier at line and col in fname. handler should be a dictionary function +" that takes a list of strings in the form 'file:line:col: message'. handler +" will be attached to a dictionary that manages state (statuslines, sets the +" winid, etc.). handler should take three arguments: an exit_code, a JSON +" object encoded to a string that mimics guru's ouput for guru implements, and +" a third parameter that only exists for compatibility with guru implements. +function! go#lsp#Implements(fname, line, col, handler) abort + call go#lsp#DidChange(a:fname) + + let l:lsp = s:lspfactory.get() + let l:msg = go#lsp#message#Implementation(a:fname, a:line, a:col) + + let l:state = s:newHandlerState('implements') + + let l:state.handleResult = funcref('s:handleImplements', [function(a:handler, [], l:state)], l:state) + let l:state.error = funcref('s:handleImplementsError', [function(a:handler, [], l:state)], l:state) + return l:lsp.sendMessage(l:msg, l:state) +endfunction + +function! s:handleImplements(next, msg) abort dict + call s:handleLocations(a:next, a:msg) +endfunction + +function! s:handleImplementsError(next, error) abort dict + call call(a:next, [1, [a:error], '']) endfunction function! go#lsp#Hover(fname, line, col, handler) abort @@ -463,24 +901,89 @@ function! go#lsp#Hover(fname, line, col, handler) abort let l:state = s:newHandlerState('') let l:state.handleResult = funcref('s:hoverHandler', [function(a:handler, [], l:state)], l:state) let l:state.error = funcref('s:noop') - call l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction function! s:hoverHandler(next, msg) abort dict - let l:content = split(a:msg.contents.value, '; ') - if len(l:content) > 1 - let l:curly = stridx(l:content[0], '{') - let l:content = extend([l:content[0][0:l:curly]], map(extend([l:content[0][l:curly+1:]], l:content[1:]), '"\t" . v:val')) - let l:content[len(l:content)-1] = '}' + if a:msg is v:null || !has_key(a:msg, 'contents') + return endif - let l:args = [l:content] - call call(a:next, l:args) + try + let l:value = json_decode(a:msg.contents.value) + let l:args = [l:value.signature] + call call(a:next, l:args) + catch + " TODO(bc): log the message and/or show an error message. + endtry +endfunction + +function! go#lsp#Doc() abort + let l:fname = expand('%:p') + let [l:line, l:col] = go#lsp#lsp#Position() + + call go#lsp#DidChange(l:fname) + + let l:lsp = s:lspfactory.get() + let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col) + let l:state = s:newHandlerState('doc') + let l:resultHandler = go#promise#New(function('s:docFromHoverResult', [], l:state), 10000, '') + let l:state.handleResult = l:resultHandler.wrapper + let l:state.error = l:resultHandler.wrapper + call l:lsp.sendMessage(l:msg, l:state) + return l:resultHandler.await() +endfunction + +function! s:docFromHoverResult(msg) abort dict + if type(a:msg) is type('') + return [a:msg, 1] + endif + + if a:msg is v:null || !has_key(a:msg, 'contents') + return ['Undocumented', 0] + endif + + let l:value = json_decode(a:msg.contents.value) + let l:doc = l:value.fullDocumentation + if len(l:doc) is 0 + let l:doc = 'Undocumented' + endif + let l:content = printf("%s\n\n%s", l:value.signature, l:doc) + return [l:content, 0] +endfunction + +function! go#lsp#DocLink() abort + let l:fname = expand('%:p') + let [l:line, l:col] = go#lsp#lsp#Position() + + call go#lsp#DidChange(l:fname) + + let l:lsp = s:lspfactory.get() + let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col) + let l:state = s:newHandlerState('doc url') + let l:resultHandler = go#promise#New(function('s:docLinkFromHoverResult', [], l:state), 10000, '') + let l:state.handleResult = l:resultHandler.wrapper + let l:state.error = l:resultHandler.wrapper + call l:lsp.sendMessage(l:msg, l:state) + return l:resultHandler.await() +endfunction + +function! s:docLinkFromHoverResult(msg) abort dict + if type(a:msg) is type('') + return [a:msg, 1] + endif + + if a:msg is v:null || !has_key(a:msg, 'contents') + return + endif + + let l:doc = json_decode(a:msg.contents.value) + return [l:doc.link, ''] endfunction function! go#lsp#Info(showstatus) let l:fname = expand('%:p') - let [l:line, l:col] = getpos('.')[1:2] + let [l:line, l:col] = go#lsp#lsp#Position() call go#lsp#DidChange(l:fname) @@ -492,19 +995,42 @@ function! go#lsp#Info(showstatus) let l:state = s:newHandlerState('') endif - let l:state.handleResult = funcref('s:infoDefinitionHandler', [function('s:info', []), a:showstatus], l:state) + let l:state.handleResult = funcref('s:infoDefinitionHandler', [function('s:info', [1], l:state), a:showstatus], l:state) + let l:state.error = funcref('s:noop') + let l:msg = go#lsp#message#Definition(l:fname, l:line, l:col) + return l:lsp.sendMessage(l:msg, l:state) +endfunction + +function! go#lsp#GetInfo() + let l:fname = expand('%:p') + let [l:line, l:col] = go#lsp#lsp#Position() + + call go#lsp#DidChange(l:fname) + + let l:lsp = s:lspfactory.get() + + let l:state = s:newHandlerState('') + + let l:info = go#promise#New(function('s:info', [0], l:state), 10000, '') + + let l:state.handleResult = funcref('s:infoDefinitionHandler', [l:info.wrapper, 0], l:state) let l:state.error = funcref('s:noop') let l:msg = go#lsp#message#Definition(l:fname, l:line, l:col) call l:lsp.sendMessage(l:msg, l:state) + return l:info.await() endfunction function! s:infoDefinitionHandler(next, showstatus, msg) abort dict " gopls returns a []Location; just take the first one. + if a:msg is v:null || len(a:msg) == 0 + return + endif + let l:msg = a:msg[0] let l:fname = go#path#FromURI(l:msg.uri) - let l:line = l:msg.range.start.line+1 - let l:col = l:msg.range.start.character+1 + let l:line = l:msg.range.start.line + let l:col = l:msg.range.start.character let l:lsp = s:lspfactory.get() let l:msg = go#lsp#message#Hover(l:fname, l:line, l:col) @@ -515,15 +1041,556 @@ function! s:infoDefinitionHandler(next, showstatus, msg) abort dict let l:state = s:newHandlerState('') endif - let l:state.handleResult = funcref('s:hoverHandler', [function('s:info', [], l:state)], l:state) + let l:state.handleResult = a:next let l:state.error = funcref('s:noop') - call l:lsp.sendMessage(l:msg, l:state) + return l:lsp.sendMessage(l:msg, l:state) endfunction -function! s:info(content) abort dict +function! s:info(show, msg) abort dict + if a:msg is v:null || !has_key(a:msg, 'contents') + return + endif + + let l:value = json_decode(a:msg.contents.value) + let l:content = [l:value.singleLine] + let l:content = s:infoFromHoverContent(l:content) + + if a:show + call go#util#ShowInfo(l:content) + endif + + return l:content +endfunction + +function! s:infoFromHoverContent(content) abort + if len(a:content) < 1 + return '' + endif + + let l:content = a:content[0] + " strip off the method set and fields of structs and interfaces. - let l:content = substitute(a:content[0], '{.*', '', '') - call go#util#ShowInfo(l:content) + if l:content =~# '^\(type \)\?[^ ]\+ \(struct\|interface\)' + let l:content = substitute(l:content, '{.*', '', '') + endif + + return l:content +endfunction + +function! go#lsp#AddWorkspaceDirectory(...) abort + if a:0 == 0 + return + endif + + call go#lsp#CleanWorkspaces() + + let l:workspaces = [] + for l:dir in a:000 + let l:dir = fnamemodify(l:dir, ':p') + if !isdirectory(l:dir) + continue + endif + + let l:workspaces = add(l:workspaces, l:dir) + endfor + + let l:lsp = s:lspfactory.get() + let l:state = s:newHandlerState('') + let l:lsp.workspaceDirectories = extend(l:lsp.workspaceDirectories, l:workspaces) + let l:msg = go#lsp#message#ChangeWorkspaceFolders(l:workspaces, []) + call l:lsp.sendMessage(l:msg, l:state) + + return 0 +endfunction + +function! go#lsp#CleanWorkspaces() abort + let l:workspaces = [] + + let l:lsp = s:lspfactory.get() + + let l:i = 0 + let l:missing = [] + for l:dir in l:lsp.workspaceDirectories + if !isdirectory(l:dir) + let l:missing = add(l:missing, l:dir) + call remove(l:lsp.workspaceDirectories, l:i) + continue + endif + let l:i += 1 + endfor + + if len(l:missing) == 0 + return 0 + endif + + let l:state = s:newHandlerState('') + let l:msg = go#lsp#message#ChangeWorkspaceFolders([], l:missing) + call l:lsp.sendMessage(l:msg, l:state) + + return 0 +endfunction + +" go#lsp#ResetWorkspaceDiretories removes and then re-adds all workspace +" folders to cause gopls to send configuration requests for all of them again. +" This is useful, for instance, when build tags have been added and gopls +" needs to use them. +function! go#lsp#ResetWorkspaceDirectories() abort + call go#lsp#CleanWorkspaces() + + let l:lsp = s:lspfactory.get() + + let l:state = s:newHandlerState('') + let l:msg = go#lsp#message#ChangeWorkspaceFolders(l:lsp.workspaceDirectories, l:lsp.workspaceDirectories) + call l:lsp.sendMessage(l:msg, l:state) + + return 0 +endfunction + +function! go#lsp#DebugBrowser() abort + let l:lsp = s:lspfactory.get() + let l:port = get(l:lsp, 'debugport', 0) + if !l:port + call go#util#EchoError("gopls was not started with debugging enabled. See :help g:go_debug.") + return + endif + + call go#util#OpenBrowser(printf('http://localhost:%d', l:port)) +endfunction + +function! go#lsp#Exit() abort + call s:exit(0) +endfunction + +function! go#lsp#Restart() abort + call s:exit(1) +endfunction + +function! s:exit(restart) abort + if !go#util#has_job() || len(s:lspfactory) == 0 || !has_key(s:lspfactory, 'current') + return + endif + + let l:lsp = s:lspfactory.get() + + " reset the factory so that future requests don't use the same instance of + " gopls. + call s:lspfactory.reset() + + let l:lsp.restarting = a:restart + + let l:state = s:newHandlerState('exit') + + let l:msg = go#lsp#message#Shutdown() + let l:retval = l:lsp.sendMessage(l:msg, l:state) + + let l:msg = go#lsp#message#Exit() + let l:retval = l:lsp.sendMessage(l:msg, l:state) + + return l:retval +endfunction + +let s:log = [] +function! s:debugasync(timer) abort + if !go#util#HasDebug('lsp') + let s:log = [] + return + endif + + let l:winid = win_getid() + + let l:name = '__GOLSP_LOG__' + let l:log_winid = bufwinid(l:name) + if l:log_winid == -1 + silent keepalt botright 10new + silent file `='__GOLSP_LOG__'` + setlocal buftype=nofile bufhidden=wipe nomodified nobuflisted noswapfile nowrap nonumber nocursorline + setlocal filetype=golsplog + else + call win_gotoid(l:log_winid) + endif + + try + setlocal modifiable + for [l:event, l:data] in s:log + call remove(s:log, 0) + if getline(1) == '' + call setline('$', printf('===== %s =====', l:event)) + else + call append('$', printf('===== %s =====', l:event)) + endif + call append('$', split(l:data, "\r\n")) + endfor + normal! G + setlocal nomodifiable + finally + call win_gotoid(l:winid) + endtry +endfunction + +function! s:debug(event, data) abort + let l:shouldStart = len(s:log) > 0 + let s:log = add(s:log, [a:event, a:data]) + + if l:shouldStart + call timer_start(10, function('s:debugasync', [])) + endif +endfunction + +function! s:compareLocations(left, right) abort + if a:left.uri < a:right.uri + return -1 + endif + + if a:left.uri == a:right.uri && a:left.range.start.line < a:right.range.start.line + return -1 + endif + + if a:left.uri == a:right.uri && a:left.range.start.line == a:right.range.start.line && a:left.range.start.character < a:right.range.start.character + return -1 + endif + + if a:left.uri == a:right.uri && a:left.range.start.line == a:right.range.start.line && a:left.range.start.character == a:right.range.start.character + return 0 + endif + + return 1 +endfunction + +function! go#lsp#Diagnostics(...) abort + if a:0 == 0 + return [] + endif + + let l:dirsToPackages = {} + + let l:lsp = s:lspfactory.get() + + let l:diagnostics = [] + for [l:key, l:val] in items(l:lsp.diagnostics) + let l:dir = fnamemodify(l:key, ':h') + + if !has_key(l:dirsToPackages, l:dir) + let l:pkg = go#package#FromPath(l:dir) + let l:dirsToPackages[l:dir] = l:pkg + else + let l:pkg = l:dirsToPackages[l:dir] + endif + + if type(l:pkg) == type(0) + continue + endif + + for l:arg in a:000 + if l:arg == l:pkg || l:arg == 'all' + let l:diagnostics = extend(l:diagnostics, l:val) + endif + endfor + endfor + + return sort(l:diagnostics) +endfunction + +function! go#lsp#AnalyzeFile(fname) abort + let l:fname = fnamemodify(a:fname, ':p') + if !isdirectory(fnamemodify(l:fname, ':h')) + return [] + endif + + let l:lsp = s:lspfactory.get() + + let l:lastdiagnostics = get(l:lsp.diagnostics, l:fname, []) + + let l:version = l:lsp.fileVersions[a:fname] + if l:version == getbufvar(a:fname, 'changedtick') + return l:lastdiagnostics + endif + + call go#lsp#DidChange(a:fname) + + let l:diagnostics = go#promise#New(function('s:setDiagnostics', []), 10000, l:lastdiagnostics) + let l:lsp.notificationQueue[l:fname] = add(l:lsp.notificationQueue[l:fname], l:diagnostics.wrapper) + return l:diagnostics.await() +endfunction + +function! s:setDiagnostics(...) abort + return a:000 +endfunction + +" s:processDiagnostic converts a diagnostic into an error string. It returns +" the errors string and the match position described in the diagnostic. The +" match position will be an empty list when bufname is not a valid name for +" the current buffer. +function! s:errorFromDiagnostic(diagnostic, bufname, fname) abort + let l:range = a:diagnostic.range + + let l:line = l:range.start.line + 1 + let l:buflines = getbufline(a:bufname, l:line) + let l:col = '' + if len(l:buflines) > 0 + let l:col = go#lsp#lsp#PositionOf(l:buflines[0], l:range.start.character) + endif + let l:error = printf('%s:%s:%s:%s: %s', a:fname, l:line, l:col, go#lsp#lsp#SeverityToErrorType(a:diagnostic.severity), a:diagnostic.message) + + if !(a:diagnostic.severity == 1 || a:diagnostic.severity == 2) + return [l:error, []] + endif + + " return when the diagnostic is not for the current buffer. + if bufnr(a:bufname) != bufnr('') + return [l:error, []] + end + + let l:endline = l:range.end.line + 1 + " don't bother trying to highlight errors or warnings that span + " the whole file (e.g when there's missing package documentation). + if l:line == 1 && (l:endline) == line('$') + return [l:error, []] + endif + let l:endcol = go#lsp#lsp#PositionOf(getline(l:endline), l:range.end.character) + + " the length of the match is the number of bytes between the start of + " the match and the end of the match. + let l:matchLength = line2byte(l:endline) + l:endcol - (line2byte(l:line) + l:col) + let l:pos = [l:line, l:col, l:matchLength] + + return [l:error, l:pos] +endfunction + +function! s:highlightMatches(errorMatches, warningMatches) abort + " set buffer variables for errors and warnings to zero values + let b:go_diagnostic_matches = {'errors': [], 'warnings': []} + + if hlexists('goDiagnosticError') + " clear the old matches just before adding the new ones to keep flicker + " to a minimum. + call go#util#ClearHighlights('goDiagnosticError') + if go#config#HighlightDiagnosticErrors() + let b:go_diagnostic_matches.errors = copy(a:errorMatches) + call go#util#HighlightPositions('goDiagnosticError', a:errorMatches) + endif + endif + + if hlexists('goDiagnosticWarning') + " clear the old matches just before adding the new ones to keep flicker + " to a minimum. + call go#util#ClearHighlights('goDiagnosticWarning') + if go#config#HighlightDiagnosticWarnings() + let b:go_diagnostic_matches.warnings = copy(a:warningMatches) + call go#util#HighlightPositions('goDiagnosticWarning', a:warningMatches) + endif + endif + + " re-apply matches at the time the buffer is displayed in a new window or + " redisplayed in an existing window: e.g. :edit, + augroup vim-go-diagnostics + autocmd! * + autocmd BufDelete autocmd! vim-go-diagnostics * + if has('textprop') + autocmd BufReadPost nested call s:highlightMatches(b:go_diagnostic_matches.errors, b:go_diagnostic_matches.warnings) + else + autocmd BufWinEnter nested call s:highlightMatches(b:go_diagnostic_matches.errors, b:go_diagnostic_matches.warnings) + endif + augroup end +endfunction + +" ClearDiagnosticHighlights removes all goDiagnosticError and +" goDiagnosticWarning matches. +function! go#lsp#ClearDiagnosticHighlights() abort + call go#util#ClearHighlights('goDiagnosticError') + call go#util#ClearHighlights('goDiagnosticWarning') +endfunction + +" Format formats the current buffer. +function! go#lsp#Format() abort + let l:fname = expand('%:p') + " send the current file so that TextEdits will be relative to the current + " state of the buffer. + call go#lsp#DidChange(l:fname) + + let l:lsp = s:lspfactory.get() + + let l:state = s:newHandlerState('') + let l:handleFormat = go#promise#New(function('s:handleFormat', [], l:state), 10000, '') + let l:state.handleResult = l:handleFormat.wrapper + let l:state.error = l:handleFormat.wrapper + let l:state.handleError = function('s:handleFormatError', [l:fname], l:state) + let l:msg = go#lsp#message#Format(l:fname) + call l:lsp.sendMessage(l:msg, l:state) + + call go#fmt#CleanErrors() + + " await the result to avoid any race conditions among autocmds (e.g. + " BufWritePre and BufWritePost) + call l:handleFormat.await() +endfunction + +" Imports executes the source.organizeImports code action for the current +" buffer. +function! go#lsp#Imports() abort + let l:fname = expand('%:p') + " send the current file so that TextEdits will be relative to the current + " state of the buffer. + call go#lsp#DidChange(l:fname) + + let l:lsp = s:lspfactory.get() + + let l:state = s:newHandlerState('') + let l:handler = go#promise#New(function('s:handleCodeAction', [], l:state), 10000, '') + let l:state.handleResult = l:handler.wrapper + let l:state.error = l:handler.wrapper + let l:state.handleError = function('s:handleCodeActionError', [l:fname], l:state) + let l:msg = go#lsp#message#CodeActionImports(l:fname) + call l:lsp.sendMessage(l:msg, l:state) + + " await the result to avoid any race conditions among autocmds (e.g. + " BufWritePre and BufWritePost) + call l:handler.await() +endfunction + +function! s:handleFormat(msg) abort dict + call go#fmt#CleanErrors() + + if type(a:msg) is type('') + call self.handleError(a:msg) + return + endif + call s:applyTextEdits(a:msg) +endfunction + +function! s:handleCodeAction(msg) abort dict + if type(a:msg) is type('') + call self.handleError(a:msg) + return + endif + + if a:msg is v:null + return + endif + + for l:item in a:msg + if get(l:item, 'kind', '') is 'source.organizeImports' + if !has_key(l:item, 'edit') + continue + endif + if !has_key(l:item.edit, 'documentChanges') + continue + endif + for l:change in l:item.edit.documentChanges + if !has_key(l:change, 'edits') + continue + endif + " TODO(bc): change to the buffer for l:change.textDocument.uri + call s:applyTextEdits(l:change.edits) + endfor + endif + endfor +endfunction + +function s:applyTextEdits(msg) abort + if a:msg is v:null + return + endif + + " process the TextEdit list in reverse order, because the positions are + " based on the current line numbers; processing in forward order would + " require keeping track of how the proper position of each TextEdit would be + " affected by all the TextEdits that came before. + call reverse(sort(a:msg, function('s:textEditLess'))) + for l:msg in a:msg + let l:startline = l:msg.range.start.line+1 + let l:endline = l:msg.range.end.line+1 + let l:text = l:msg.newText + + " handle the deletion of whole lines + if len(l:text) == 0 && l:msg.range.start.character == 0 && l:msg.range.end.character == 0 && l:startline < l:endline + call s:deleteline(l:startline, l:endline-1) + continue + endif + + let l:startcontent = getline(l:startline) + let l:preSliceEnd = 0 + if l:msg.range.start.character > 0 + let l:preSliceEnd = go#lsp#lsp#PositionOf(l:startcontent, l:msg.range.start.character-1) - 1 + let l:startcontent = l:startcontent[:l:preSliceEnd] + elseif l:endline == l:startline && (l:msg.range.end.character == 0 || l:msg.range.start.character == 0) + " l:startcontent should be the empty string when l:text is a + " replacement at the beginning of the line. + let l:startcontent = '' + endif + + let l:endcontent = getline(l:endline) + let l:postSliceStart = 0 + if l:msg.range.end.character > 0 + let l:postSliceStart = go#lsp#lsp#PositionOf(l:endcontent, l:msg.range.end.character-1) + let l:endcontent = l:endcontent[(l:postSliceStart):] + endif + + " There isn't an easy way to replace the text in a byte or character + " range, so append to l:text any text on l:endline starting from + " l:postSliceStart and prepend to l:text any text on l:startline prior to + " l:preSliceEnd, and finally replace the lines with a delete followed by + " and append. + let l:text = printf('%s%s%s', l:startcontent, l:text, l:endcontent) + + " TODO(bc): deal with the undo file + " TODO(bc): deal with folds + + call s:deleteline(l:startline, l:endline) + for l:line in split(l:text, "\n", 1) + call append(l:startline-1, l:line) + let l:startline += 1 + endfor + endfor + + call go#lsp#DidChange(expand('%:p')) + return +endfunction + +function! s:handleFormatError(filename, msg) abort dict + if go#config#FmtFailSilently() + return + endif + + let l:errors = split(a:msg, '\n') + let l:errors = map(l:errors, printf('substitute(v:val, ''^'', ''%s:'', '''')', a:filename)) + let l:errors = join(l:errors, "\n") + call go#fmt#ShowErrors(l:errors) +endfunction + +function! s:handleCodeActionError(filename, msg) abort dict + " TODO(bc): handle the error? +endfunction + +function! s:textEditLess(left, right) abort + " TextEdits in a TextEdit[] never overlap and Vim's sort() is stable. + if a:left.range.start.line < a:right.range.start.line + return -1 + endif + + if a:left.range.start.line > a:right.range.start.line + return 1 + endif + + if a:left.range.start.line == a:right.range.start.line + if a:left.range.start.character < a:right.range.start.character + return -1 + endif + + if a:left.range.start.character > a:right.range.start.character + return 1 + endif + endif + + " return 0, because a:left an a:right refer to the same position. + return 0 +endfunction + +function! s:deleteline(start, end) abort + if exists('*deletebufline') + call deletebufline('', a:start, a:end) + else + call execute(printf('%d,%d d_', a:start, a:end)) + endif endfunction " restore Vi compatibility settings diff --git a/pack/acp/start/vim-go/autoload/go/lsp/completionitemkind.vim b/pack/acp/start/vim-go/autoload/go/lsp/completionitemkind.vim index 37c00a8..0202bd2 100644 --- a/pack/acp/start/vim-go/autoload/go/lsp/completionitemkind.vim +++ b/pack/acp/start/vim-go/autoload/go/lsp/completionitemkind.vim @@ -28,7 +28,7 @@ let s:Event = 23 let s:Operator = 24 let s:TypeParameter = 25 -function! go#lsp#completionitemkind#Vim(kind) +function! go#lsp#completionitemkind#Vim(kind) abort if a:kind == s:Method || a:kind == s:Function || a:kind == s:Constructor return 'f' elseif a:kind == s:Variable || a:kind == s:Constant @@ -40,6 +40,22 @@ function! go#lsp#completionitemkind#Vim(kind) endif endfunction +function! go#lsp#completionitemkind#IsFunction(kind) abort + if a:kind == s:Function + return 1 + endif + + return 0 +endfunction + +function! go#lsp#completionitemkind#IsMethod(kind) abort + if a:kind == s:Method + return 1 + endif + + return 0 +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/lsp/lsp.vim b/pack/acp/start/vim-go/autoload/go/lsp/lsp.vim new file mode 100644 index 0000000..c0cf3e0 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/lsp/lsp.vim @@ -0,0 +1,72 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +" go#lsp#lsp#Position returns the LSP text position. If no arguments are +" provided, the cursor position is assumed. Otherwise, there should be two +" arguments: the line and the column. +function! go#lsp#lsp#Position(...) + if a:0 < 2 + let [l:line, l:col] = getpos('.')[1:2] + else + let l:line = a:1 + let l:col = a:2 + endif + let l:content = getline(l:line) + + " LSP uses 0-based lines. + return [l:line - 1, s:character(l:line, l:col-1)] +endfunction + +function! s:strlen(str) abort + let l:runes = split(a:str, '\zs') + return len(l:runes) + len(filter(l:runes, 'char2nr(v:val)>=0x10000')) +endfunction + +function! s:character(line, col) abort + return s:strlen(getline(a:line)[:col([a:line, a:col - 1])]) +endfunction + +" go#lsp#PositionOf returns len(content[0:units]) where units is utf-16 code +" units. This is mostly useful for converting LSP text position to vim +" position. +function! go#lsp#lsp#PositionOf(content, units, ...) abort + if a:units == 0 + return 1 + endif + + let l:remaining = a:units + let l:str = '' + for l:rune in split(a:content, '\zs') + if l:remaining < 0 + break + endif + let l:remaining -= 1 + if char2nr(l:rune) >= 0x10000 + let l:remaining -= 1 + endif + let l:str = l:str . l:rune + endfor + + return len(l:str) +endfunction + +function! go#lsp#lsp#SeverityToErrorType(severity) abort + if a:severity == 1 + return 'E' + elseif a:severity == 2 + return 'W' + elseif a:severity == 3 + return 'I' + elseif a:severity == 4 + return 'I' + endif + + return '' +endfunction + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/lsp/lsp_test.vim b/pack/acp/start/vim-go/autoload/go/lsp/lsp_test.vim new file mode 100644 index 0000000..24a2a74 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/lsp/lsp_test.vim @@ -0,0 +1,32 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +scriptencoding utf-8 + +function! Test_PositionOf_Simple() + let l:actual = go#lsp#lsp#PositionOf("just ascii", 3) + call assert_equal(4, l:actual) +endfunc + + +function! Test_PositionOf_MultiByte() + " ⌘ is U+2318, which encodes to three bytes in utf-8 and 1 code unit in + " utf-16. + let l:actual = go#lsp#lsp#PositionOf("⌘⌘ foo", 3) + call assert_equal(8, l:actual) +endfunc + +function! Test_PositionOf_MultipleCodeUnit() + " 𐐀 is U+10400, which encodes to 4 bytes in utf-8 and 2 code units in + " utf-16. + let l:actual = go#lsp#lsp#PositionOf("𐐀 bar", 3) + call assert_equal(6, l:actual) +endfunction + + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/lsp/message.vim b/pack/acp/start/vim-go/autoload/go/lsp/message.vim index 6398185..90f9f5b 100644 --- a/pack/acp/start/vim-go/autoload/go/lsp/message.vim +++ b/pack/acp/start/vim-go/autoload/go/lsp/message.vim @@ -10,17 +10,100 @@ function! go#lsp#message#Initialize(wd) abort \ 'processId': getpid(), \ 'rootUri': go#path#ToURI(a:wd), \ 'capabilities': { - \ 'workspace': {}, + \ 'workspace': { + \ 'workspaceFolders': v:true, + \ 'didChangeConfiguration': { + \ 'dynamicRegistration': v:true, + \ }, + \ 'configuration': v:true, + \ }, \ 'textDocument': { \ 'hover': { \ 'contentFormat': ['plaintext'], \ }, + \ 'completion': { + \ 'completionItem': { + \ 'snippetSupport': go#config#GoplsUsePlaceholders() ? v:true : v:false, + \ }, + \ }, + \ 'codeAction': { + \ 'codeActionLiteralSupport': { + \ 'codeActionKind': { + \ 'valueSet': ['source.organizeImports'], + \ }, + \ }, + \ }, \ } - \ } + \ }, + \ 'workspaceFolders': [s:workspaceFolder(0, a:wd)], \ } \ } endfunction +function! go#lsp#message#Initialized() abort + return { + \ 'notification': 1, + \ 'method': 'initialized', + \ 'params': {}, + \ } +endfunction + +function! go#lsp#message#Shutdown() abort + return { + \ 'notification': 0, + \ 'method': 'shutdown', + \ } +endfunction + +function! go#lsp#message#Format(file) abort + return { + \ 'notification': 0, + \ 'method': 'textDocument/formatting', + \ 'params': { + \ 'textDocument': { + \ 'uri': go#path#ToURI(a:file) + \ }, + \ 'options': { + \ 'insertSpaces': v:false, + \ }, + \ } + \ } +endfunction + +function! go#lsp#message#CodeActionImports(file) abort + return s:codeAction('source.organizeImports', a:file) +endfunction + +function! s:codeAction(name, file) abort + return { + \ 'notification': 0, + \ 'method': 'textDocument/codeAction', + \ 'params': { + \ 'textDocument': { + \ 'uri': go#path#ToURI(a:file) + \ }, + \ 'range': { + \ 'start': {'line': 0, 'character': 0}, + \ 'end': {'line': line('$'), 'character': 0}, + \ }, + \ 'context': { + \ 'only': [a:name], + \ }, + \ } + \ } +endfunction + +function! go#lsp#message#Exit() abort + return { + \ 'notification': 1, + \ 'method': 'exit', + \ } +endfunction + +function! go#lsp#message#WorkspaceFoldersResult(dirs) abort + return map(copy(a:dirs), function('s:workspaceFolder', [])) +endfunction + function! go#lsp#message#Definition(file, line, col) abort return { \ 'notification': 0, @@ -47,7 +130,20 @@ function! go#lsp#message#TypeDefinition(file, line, col) abort \ } endfunction -function! go#lsp#message#DidOpen(file, content) abort +function! go#lsp#message#Implementation(file, line, col) abort + return { + \ 'notification': 0, + \ 'method': 'textDocument/implementation', + \ 'params': { + \ 'textDocument': { + \ 'uri': go#path#ToURI(a:file) + \ }, + \ 'position': s:position(a:line, a:col) + \ } + \ } +endfunction + +function! go#lsp#message#DidOpen(file, content, version) abort return { \ 'notification': 1, \ 'method': 'textDocument/didOpen', @@ -56,18 +152,20 @@ function! go#lsp#message#DidOpen(file, content) abort \ 'uri': go#path#ToURI(a:file), \ 'languageId': 'go', \ 'text': a:content, + \ 'version': a:version, \ } \ } \ } endfunction -function! go#lsp#message#DidChange(file, content) abort +function! go#lsp#message#DidChange(file, content, version) abort return { \ 'notification': 1, \ 'method': 'textDocument/didChange', \ 'params': { \ 'textDocument': { \ 'uri': go#path#ToURI(a:file), + \ 'version': a:version, \ }, \ 'contentChanges': [ \ { @@ -103,6 +201,22 @@ function! go#lsp#message#Completion(file, line, col) abort \ } endfunction +function! go#lsp#message#References(file, line, col) abort + return { + \ 'notification': 0, + \ 'method': 'textDocument/references', + \ 'params': { + \ 'textDocument': { + \ 'uri': go#path#ToURI(a:file) + \ }, + \ 'position': s:position(a:line, a:col), + \ 'context': { + \ 'includeDeclaration': v:true, + \ }, + \ } + \ } +endfunction + function! go#lsp#message#Hover(file, line, col) abort return { \ 'notification': 0, @@ -116,8 +230,120 @@ function! go#lsp#message#Hover(file, line, col) abort \ } endfunction +function! go#lsp#message#ChangeWorkspaceFolders(add, remove) abort + let l:addDirs = map(copy(a:add), function('s:workspaceFolder', [])) + let l:removeDirs = map(copy(a:remove), function('s:workspaceFolder', [])) + + return { + \ 'notification': 1, + \ 'method': 'workspace/didChangeWorkspaceFolders', + \ 'params': { + \ 'event': { + \ 'removed': l:removeDirs, + \ 'added': l:addDirs, + \ }, + \ } + \ } + +endfunction + +function! go#lsp#message#ConfigurationResult(items) abort + let l:result = [] + + " results must be in the same order as the items + for l:item in a:items + let l:config = { + \ 'buildFlags': [], + \ 'hoverKind': 'Structured', + \ } + let l:buildtags = go#config#BuildTags() + if buildtags isnot '' + let l:config.buildFlags = extend(l:config.buildFlags, ['-tags', go#config#BuildTags()]) + endif + + let l:deepCompletion = go#config#GoplsDeepCompletion() + let l:matcher = go#config#GoplsMatcher() + let l:completeUnimported = go#config#GoplsCompleteUnimported() + let l:staticcheck = go#config#GoplsStaticCheck() + let l:usePlaceholder = go#config#GoplsUsePlaceholders() + let l:tempModfile = go#config#GoplsTempModfile() + let l:analyses = go#config#GoplsAnalyses() + let l:local = go#config#GoplsLocal() + let l:gofumpt = go#config#GoplsGofumpt() + let l:settings = go#config#GoplsSettings() + + if l:deepCompletion isnot v:null + if l:deepCompletion + let l:config.deepCompletion = v:true + else + let l:config.deepCompletion = v:false + endif + endif + + if l:matcher isnot v:null + let l:config.matcher = l:matcher + endif + + if l:completeUnimported isnot v:null + if l:completeUnimported + let l:config.completeUnimported = v:true + else + let l:config.completeUnimported = v:false + endif + endif + + if l:staticcheck isnot v:null + if l:staticcheck + let l:config.staticcheck = v:true + else + let l:config.staticcheck = v:false + endif + endif + + if l:usePlaceholder isnot v:null + if l:usePlaceholder + let l:config.usePlaceholders = v:true + else + let l:config.usePlaceholders = v:false + endif + endif + + if l:tempModfile isnot v:null + if l:tempModfile + let l:config.tempModfile = v:true + else + let l:config.tempModfile = v:false + endif + endif + + if l:analyses isnot v:null + let l:config.analyses = l:analyses + endif + + if l:local isnot v:null + let l:config.local = l:local + endif + + if l:gofumpt isnot v:null + let l:config.gofumpt = l:gofumpt + endif + + if l:settings isnot v:null + let l:config = extend(l:config, l:settings, 'keep') + endif + + let l:result = add(l:result, l:config) + endfor + + return l:result +endfunction + +function s:workspaceFolder(key, val) abort + return {'uri': go#path#ToURI(a:val), 'name': a:val} +endfunction + function! s:position(line, col) abort - return {'line': a:line - 1, 'character': a:col-1} + return {'line': a:line, 'character': a:col} endfunction " restore Vi compatibility settings diff --git a/pack/acp/start/vim-go/autoload/go/lsp_test.vim b/pack/acp/start/vim-go/autoload/go/lsp_test.vim new file mode 100644 index 0000000..55bf8ce --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/lsp_test.vim @@ -0,0 +1,97 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +scriptencoding utf-8 + +function! Test_GetSimpleTextPosition() + call s:getinfo('lsp text position should align with cursor position after ascii only', 'ascii') +endfunction + +function! Test_GetMultiByteTextPosition() + call s:getinfo('lsp text position should align with cursor position after two place of interest symbols ⌘⌘', 'multi-byte') +endfunction + +function! Test_GetMultipleCodeUnitTextPosition() + call s:getinfo('lsp text position should align with cursor position after Deseret Capital Letter Long I 𐐀', 'multi-code-units') +endfunction + +function! s:getinfo(str, name) + if !go#util#has_job() + return + endif + + try + let g:go_info_mode = 'gopls' + + let l:tmp = gotest#write_file(a:name . '/position/position.go', [ + \ 'package position', + \ '', + \ 'func Example() {', + \ "\tid := " . '"foo"', + \ "\tprintln(" .'"' . a:str . '", id)', + \ '}', + \ ] ) + + let l:expected = 'var id string' + let l:actual = go#lsp#GetInfo() + call assert_equal(l:expected, l:actual) + finally + call delete(l:tmp, 'rf') + unlet g:go_info_mode + endtry +endfunction + +func! Test_Format() abort + try + let expected = join(readfile("test-fixtures/lsp/fmt/format_golden.go"), "\n") + let l:tmp = gotest#load_fixture('lsp/fmt/format.go') + + call go#lsp#Format() + + " this should now contain the formatted code + let actual = join(go#util#GetLines(), "\n") + + call assert_equal(expected, actual) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! Test_Format_SingleNewline() abort + try + let expected = join(readfile("test-fixtures/lsp/fmt/format_golden.go"), "\n") + let l:tmp = gotest#load_fixture('lsp/fmt/newline.go') + + call go#lsp#Format() + + " this should now contain the formatted code + let actual = join(go#util#GetLines(), "\n") + + call assert_equal(expected, actual) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +func! Test_Imports() abort + try + let expected = join(readfile("test-fixtures/lsp/imports/imports_golden.go"), "\n") + let l:tmp = gotest#load_fixture('lsp/imports/imports.go') + + call go#lsp#Imports() + + " this should now contain the expected imports code + let actual = join(go#util#GetLines(), "\n") + + call assert_equal(expected, actual) + finally + call delete(l:tmp, 'rf') + endtry +endfunc + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/mod.vim b/pack/acp/start/vim-go/autoload/go/mod.vim index 021f7f9..a38aabe 100644 --- a/pack/acp/start/vim-go/autoload/go/mod.vim +++ b/pack/acp/start/vim-go/autoload/go/mod.vim @@ -7,11 +7,15 @@ let s:go_major_version = "" function! go#mod#Format() abort " go mod only exists in `v1.11` if empty(s:go_major_version) - let tokens = matchlist(go#util#System("go version"), '\d\+.\(\d\+\)\(\.\d\+\)\? ') - let s:go_major_version = str2nr(tokens[1]) + let tokens = matchlist(go#util#Exec(['go', 'version']), '\d\+.\(\d\+\)\(\.\d\+\)\? ') + if len(tokens) > 0 + let s:go_major_version = str2nr(tokens[1]) + else + let s:go_major_version = "" + endif endif - if s:go_major_version < "11" + if !empty(s:go_major_version) && s:go_major_version < "11" call go#util#EchoError("Go v1.11 is required to format go.mod file") return endif @@ -79,18 +83,11 @@ function! go#mod#update_file(source, target) let l:listtype = go#list#Type("GoModFmt") - " the title information was introduced with 7.4-2200 - " https://github.com/vim/vim/commit/d823fa910cca43fec3c31c030ee908a14c272640 - if has('patch-7.4.2200') - " clean up previous list - if l:listtype == "quickfix" - let l:list_title = getqflist({'title': 1}) - else - let l:list_title = getloclist(0, {'title': 1}) - endif + " clean up previous list + if l:listtype == "quickfix" + let l:list_title = getqflist({'title': 1}) else - " can't check the title, so assume that the list was for go fmt. - let l:list_title = {'title': 'Format'} + let l:list_title = getloclist(0, {'title': 1}) endif if has_key(l:list_title, "title") && l:list_title['title'] == "Format" diff --git a/pack/acp/start/vim-go/autoload/go/package.vim b/pack/acp/start/vim-go/autoload/go/package.vim index ced39cc..59c7101 100644 --- a/pack/acp/start/vim-go/autoload/go/package.vim +++ b/pack/acp/start/vim-go/autoload/go/package.vim @@ -39,7 +39,7 @@ function! s:paths() abort if executable('go') let s:goroot = go#util#env("goroot") if go#util#ShellError() != 0 - echomsg '''go env GOROOT'' failed' + call go#util#EchoError('`go env GOROOT` failed') endif else let s:goroot = $GOROOT @@ -82,6 +82,10 @@ function! s:vendordirs() abort if l:err != 0 return [] endif + if empty(l:root) + return [] + endif + let l:root = split(l:root, '\n')[0] . go#util#PathSep() . 'src' let [l:dir, l:err] = go#util#ExecInDir(['go', 'list', '-f', '{{.Dir}}']) @@ -111,55 +115,77 @@ function! s:vendordirs() abort endfunction let s:import_paths = {} -" ImportPath returns the import path of the package for current buffer. +" ImportPath returns the import path of the package for current buffer. It +" returns -1 if the import path cannot be determined. function! go#package#ImportPath() abort - let dir = expand("%:p:h") + let l:dir = expand("%:p:h") if has_key(s:import_paths, dir) - return s:import_paths[dir] + return s:import_paths[l:dir] endif - let [l:out, l:err] = go#util#ExecInDir(['go', 'list']) - if l:err != 0 + let l:importpath = go#package#FromPath(l:dir) + if type(l:importpath) == type(0) return -1 endif - let l:importpath = split(out, '\n')[0] - - " go list returns '_CURRENTDIRECTORY' if the directory is not inside GOPATH. - " Check it and retun an error if that is the case - if l:importpath[0] ==# '_' - return -1 - endif - - let s:import_paths[dir] = l:importpath + let s:import_paths[l:dir] = l:importpath return l:importpath endfunction - -" FromPath returns the import path of arg. +" go#package#FromPath returns the import path of arg. -1 is returned when arg +" does not specify a package. -2 is returned when arg is a relative path +" outside of GOPATH, not in a module, and not below the current working +" directory. A relative path is returned when in a null module at or below the +" current working directory.. function! go#package#FromPath(arg) abort let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd' : 'cd' let l:dir = getcwd() - let l:path = a:arg + let l:path = fnamemodify(a:arg, ':p') if !isdirectory(l:path) let l:path = fnamemodify(l:path, ':h') endif execute l:cd fnameescape(l:path) - let [l:out, l:err] = go#util#Exec(['go', 'list']) - execute l:cd fnameescape(l:dir) - if l:err != 0 - return -1 - endif + try + if glob("*.go") == "" + " There's no Go code in this directory. We might be in a module directory + " which doesn't have any code at this level. To avoid `go list` making a + " bunch of HTTP requests to fetch dependencies, short-circuit `go list` + " and return -1 immediately. + if !empty(s:module()) + return -1 + endif + endif + let [l:out, l:err] = go#util#Exec(['go', 'list']) + if l:err != 0 + return -1 + endif - let l:importpath = split(l:out, '\n')[0] + let l:importpath = split(l:out, '\n')[0] + finally + execute l:cd fnameescape(l:dir) + endtry - " go list returns '_CURRENTDIRECTORY' if the directory is not inside GOPATH. - " Check it and retun an error if that is the case + " go list returns '_CURRENTDIRECTORY' if the directory is in a null module + " (i.e. neither in GOPATH nor in a module). Return a relative import path + " if possible or an error if that is the case. if l:importpath[0] ==# '_' - return -1 + let l:relativeimportpath = fnamemodify(l:importpath[1:], ':.') + if go#util#IsWin() + let l:relativeimportpath = substitute(l:relativeimportpath, '\\', '/', 'g') + endif + + if l:relativeimportpath == l:importpath[1:] + return '.' + endif + + if l:relativeimportpath[0] == '/' + return -2 + endif + + let l:importpath= printf('./%s', l:relativeimportpath) endif return l:importpath @@ -206,10 +232,17 @@ function! go#package#Complete(ArgLead, CmdLine, CursorPos) abort let vendordirs = s:vendordirs() + let l:modcache = go#util#env('gomodcache') + let ret = {} for dir in dirs " this may expand to multiple lines let root = split(expand(dir . '/pkg/' . s:goos . '_' . s:goarch), "\n") + if l:modcache != '' + let root = add(root, l:modcache) + else + let root = add(root, expand(dir . '/pkg/mod')) + endif let root = add(root, expand(dir . '/src'), ) let root = extend(root, vendordirs) let root = add(root, module) @@ -234,9 +267,8 @@ function! go#package#Complete(ArgLead, CmdLine, CursorPos) abort let glob = module.dir endif elseif stridx(module.path, a:ArgLead) == 0 && stridx(module.path, '/', len(a:ArgLead)) < 0 - " use the module directory when a:ArgLead is contained in - " module.path and module.path does not have any path segments after - " a:ArgLead. + " use the module directory when module.path begins wih a:ArgLead and + " module.path does not have any path segments after a:ArgLead. let glob = module.dir else continue @@ -251,6 +283,11 @@ function! go#package#Complete(ArgLead, CmdLine, CursorPos) abort if fnamemodify(candidate, ':t') == 'vendor' continue endif + " if path contains version info, strip it out + let vidx = strridx(candidate, '@') + if vidx >= 0 + let candidate = strpart(candidate, 0, vidx) + endif let candidate .= '/' elseif candidate !~ '\.a$' continue diff --git a/pack/acp/start/vim-go/autoload/go/path.vim b/pack/acp/start/vim-go/autoload/go/path.vim index 656afec..0d81d45 100644 --- a/pack/acp/start/vim-go/autoload/go/path.vim +++ b/pack/acp/start/vim-go/autoload/go/path.vim @@ -28,11 +28,11 @@ function! go#path#GoPath(...) abort let s:initial_go_path = "" endif - echon "vim-go: " | echohl Function | echon "GOPATH restored to ". $GOPATH | echohl None + call go#util#EchoInfo("GOPATH restored to ". $GOPATH) return endif - echon "vim-go: " | echohl Function | echon "GOPATH changed to ". a:1 | echohl None + call go#util#EchoInfo("GOPATH changed to ". a:1) let s:initial_go_path = $GOPATH let $GOPATH = a:1 endfunction diff --git a/pack/acp/start/vim-go/autoload/go/play.vim b/pack/acp/start/vim-go/autoload/go/play.vim index 117af25..980421e 100644 --- a/pack/acp/start/vim-go/autoload/go/play.vim +++ b/pack/acp/start/vim-go/autoload/go/play.vim @@ -4,7 +4,7 @@ set cpo&vim function! go#play#Share(count, line1, line2) abort if !executable('curl') - echohl ErrorMsg | echomsg "vim-go: require 'curl' command" | echohl None + call go#util#EchoError('cannot share: curl cannot be found') return endif @@ -20,8 +20,7 @@ function! go#play#Share(count, line1, line2) abort call delete(share_file) if l:err != 0 - echom 'A error has occurred. Run this command to see what the problem is:' - echom go#util#Shelljoin(l:cmd) + call go#util#EchoError(['A error has occurred. Run this command to see what the problem is:', go#util#Shelljoin(l:cmd)]) return endif @@ -38,7 +37,7 @@ function! go#play#Share(count, line1, line2) abort call go#util#OpenBrowser(url) endif - echo "vim-go: snippet uploaded: ".url + call go#util#EchoInfo('snippet uploaded: ' . url) endfunction diff --git a/pack/acp/start/vim-go/autoload/go/promise.vim b/pack/acp/start/vim-go/autoload/go/promise.vim new file mode 100644 index 0000000..4782883 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/promise.vim @@ -0,0 +1,58 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +scriptencoding utf-8 + +" New returns a promise. A promise's primary purpose is to make async jobs +" synchronous by awaiting fn. +" +" A promise is a dictionary with two keys: +" 'wrapper': +" A function that wraps fn. It can be used in place of fn. +" 'await': +" A function that waits for wrapper to be called and returns the value +" returned by fn. Returns default if timeout expires. +function! go#promise#New(fn, timeout, default) abort + let l:state = {} + + " explicitly bind to state so that within l:promise's methods, self will + " always refer to state. See :help Partial for more information. + return { + \ 'wrapper': function('s:wrapper', [a:fn, a:default], l:state), + \ 'await': function('s:await', [a:timeout, a:default], l:state), + \ } +endfunction + +function! s:wrapper(fn, default, ...) dict + try + let self.retval = call(a:fn, a:000) + catch + let self.retval = substitute(v:exception, '^Vim', '', '') + let self.exception = 1 + endtry + return self.retval +endfunction + +function! s:await(timeout, default) dict + let l:timer = timer_start(a:timeout, function('s:setretval', [a:default], self)) + while !has_key(self, 'retval') + sleep 50m + endwhile + call timer_stop(l:timer) + + if get(self, 'exception', 0) + throw self.retval + endif + return self.retval +endfunction + +function! s:setretval(val, timer) dict + let self.retval = a:val +endfunction + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/promise_test.vim b/pack/acp/start/vim-go/autoload/go/promise_test.vim new file mode 100644 index 0000000..f4473fe --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/promise_test.vim @@ -0,0 +1,41 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +func! Test_PromiseNew() abort + let l:sut = go#promise#New(function('s:work', []), 100, -1) + call assert_true(has_key(l:sut, 'wrapper')) + call assert_true(has_key(l:sut, 'await')) +endfunc + +func! Test_PromiseAwait() abort + let l:expected = 1 + let l:default = -1 + let l:sut = go#promise#New(function('s:work', [l:expected]), 100, l:default) + + call timer_start(10, l:sut.wrapper) + + let l:actual = call(l:sut.await, []) + call assert_equal(l:expected, l:actual) +endfunc + +func! Test_PromiseAwait_Timeout() abort + let l:desired = 1 + let l:expected = -1 + let l:sut = go#promise#New(function('s:work', [l:desired]), 10, l:expected) + + call timer_start(100, l:sut.wrapper) + + let l:actual = call(l:sut.await, []) + call assert_equal(l:expected, l:actual) +endfunc + +func! s:work(val, timer) + return a:val +endfunc + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/referrers.vim b/pack/acp/start/vim-go/autoload/go/referrers.vim new file mode 100644 index 0000000..223fec6 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/referrers.vim @@ -0,0 +1,43 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +function! go#referrers#Referrers(selected) abort + let l:mode = go#config#ReferrersMode() + if l:mode == 'guru' + call go#guru#Referrers(a:selected) + return + elseif l:mode == 'gopls' + if !go#config#GoplsEnabled() + call go#util#EchoError("go_referrers_mode is 'gopls', but gopls is disabled") + endif + let [l:line, l:col] = getpos('.')[1:2] + let [l:line, l:col] = go#lsp#lsp#Position(l:line, l:col) + let l:fname = expand('%:p') + call go#lsp#Referrers(l:fname, l:line, l:col, funcref('s:parse_output')) + return + else + call go#util#EchoWarning('unknown value for g:go_referrers_mode') + endif +endfunction + +" This uses Vim's errorformat to parse the output and put it into a quickfix +" or locationlist. +function! s:parse_output(exit_val, output, title) abort + if a:exit_val + call go#util#EchoError(a:output) + return + endif + + let errformat = ",%f:%l:%c:\ %m" + let l:listtype = go#list#Type("GoReferrers") + call go#list#ParseFormat(l:listtype, errformat, a:output, a:title, 0) + + let errors = go#list#Get(l:listtype) + call go#list#Window(l:listtype, len(errors)) +endfunction +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/rename.vim b/pack/acp/start/vim-go/autoload/go/rename.vim index 30ff590..f5d167f 100644 --- a/pack/acp/start/vim-go/autoload/go/rename.vim +++ b/pack/acp/start/vim-go/autoload/go/rename.vim @@ -20,8 +20,10 @@ function! go#rename#Rename(bang, ...) abort let to_identifier = a:1 endif + let l:bin = go#config#RenameCommand() + " return with a warning if the bin doesn't exist - let bin_path = go#path#CheckBinPath(go#config#GorenameBin()) + let bin_path = go#path#CheckBinPath(l:bin) if empty(bin_path) return endif @@ -29,7 +31,18 @@ function! go#rename#Rename(bang, ...) abort let fname = expand('%:p') let pos = go#util#OffsetCursor() let offset = printf('%s:#%d', fname, pos) - let cmd = [bin_path, "-offset", offset, "-to", to_identifier, '-tags', go#config#BuildTags()] + + let args = [] + if l:bin == 'gorename' + let l:args = extend(l:args, ['-tags', go#config#BuildTags(), '-offset', offset, '-to', to_identifier]) + elseif l:bin == 'gopls' + " TODO(bc): use -tags when gopls supports it + let l:args = extend(l:args, ['rename', '-w', l:offset, to_identifier]) + else + call go#util#EchoWarning('unexpected rename command') + endif + + let l:cmd = extend([l:bin_path], l:args) if go#util#has_job() call s:rename_job({ @@ -39,7 +52,12 @@ function! go#rename#Rename(bang, ...) abort return endif - let [l:out, l:err] = go#util#ExecInDir(l:cmd) + let l:wd = go#util#ModuleRoot() + if l:wd == -1 + let l:wd = expand("%:p:h") + endif + + let [l:out, l:err] = go#util#ExecInWorkDir(l:cmd, l:wd) call s:parse_errors(l:err, a:bang, split(l:out, '\n')) endfunction @@ -54,6 +72,11 @@ function s:rename_job(args) call go#cmd#autowrite() let l:cbs = go#job#Options(l:job_opts) + let l:wd = go#util#ModuleRoot() + if l:wd != -1 + let l:cbs.cwd = l:wd + endif + " wrap l:cbs.exit_cb in s:exit_cb. let l:cbs.exit_cb = funcref('s:exit_cb', [l:cbs.exit_cb]) diff --git a/pack/acp/start/vim-go/autoload/go/statusline.vim b/pack/acp/start/vim-go/autoload/go/statusline.vim index f0ec5cd..9779d76 100644 --- a/pack/acp/start/vim-go/autoload/go/statusline.vim +++ b/pack/acp/start/vim-go/autoload/go/statusline.vim @@ -27,9 +27,8 @@ let s:last_status = "" " if not it returns an empty string. This function should be plugged directly " into the statusline. function! go#statusline#Show() abort - " lazy initialiation of the cleaner + " lazy initialization of the cleaner if !s:timer_id - " clean every 60 seconds all statuses let interval = go#config#StatuslineDuration() let s:timer_id = timer_start(interval, function('go#statusline#Clear'), {'repeat': -1}) endif @@ -57,9 +56,9 @@ function! go#statusline#Show() abort " only update highlight if status has changed. if status_text != s:last_status - if status.state =~ "success" || status.state =~ "finished" || status.state =~ "pass" + if status.state =~ "success" || status.state =~ "finished" || status.state =~ "pass" || status.state =~ 'initialized' hi goStatusLineColor cterm=bold ctermbg=76 ctermfg=22 guibg=#5fd700 guifg=#005f00 - elseif status.state =~ "started" || status.state =~ "analysing" || status.state =~ "compiling" + elseif status.state =~ "started" || status.state =~ "analysing" || status.state =~ "compiling" || status.state =~ 'initializing' hi goStatusLineColor cterm=bold ctermbg=208 ctermfg=88 guibg=#ff8700 guifg=#870000 elseif status.state =~ "failed" hi goStatusLineColor cterm=bold ctermbg=196 ctermfg=52 guibg=#ff0000 guifg=#5f0000 @@ -83,10 +82,11 @@ function! go#statusline#Update(status_dir, status) abort " before we stop the timer, check if we have any previous jobs to be cleaned " up. Otherwise every job will reset the timer when this function is called " and thus old jobs will never be cleaned - call go#statusline#Clear(0) + call s:clear() " also reset the timer, so the user has time to see it in the statusline. - " Setting the timer_id to 0 will trigger a new cleaner routine. + " Setting the timer_id to 0 will cause a new timer to be created the next + " time the go#statusline#Show() is called. call timer_stop(s:timer_id) let s:timer_id = 0 endfunction @@ -94,6 +94,10 @@ endfunction " Clear clears all currently stored statusline data. The timer_id argument is " just a placeholder so we can pass it to a timer_start() function if needed. function! go#statusline#Clear(timer_id) abort + call s:clear() +endfunction + +function! s:clear() for [status_dir, status] in items(s:statuses) let elapsed_time = reltimestr(reltime(status.created_at)) " strip whitespace diff --git a/pack/acp/start/vim-go/autoload/go/tags.vim b/pack/acp/start/vim-go/autoload/go/tags.vim index c1239b0..2feeaa5 100644 --- a/pack/acp/start/vim-go/autoload/go/tags.vim +++ b/pack/acp/start/vim-go/autoload/go/tags.vim @@ -98,7 +98,7 @@ func s:write_out(out) abort if has_key(result, 'errors') let l:winnr = winnr() let l:listtype = go#list#Type("GoModifyTags") - call go#list#ParseFormat(l:listtype, "%f:%l:%c:%m", result['errors'], "gomodifytags") + call go#list#ParseFormat(l:listtype, "%f:%l:%c:%m", result['errors'], "gomodifytags", 0) call go#list#Window(l:listtype, len(result['errors'])) "prevent jumping to quickfix list @@ -124,6 +124,7 @@ func s:create_cmd(args) abort let l:mode = a:args.mode let l:cmd_args = a:args.cmd_args let l:modifytags_transform = go#config#AddtagsTransform() + let l:modifytags_skip_unexported = go#config#AddtagsSkipUnexported() " start constructing the command let cmd = [bin_path] @@ -131,6 +132,10 @@ func s:create_cmd(args) abort call extend(cmd, ["-file", a:args.fname]) call extend(cmd, ["-transform", l:modifytags_transform]) + if l:modifytags_skip_unexported + call extend(cmd, ["-skip-unexported"]) + endif + if has_key(a:args, "modified") call add(cmd, "-modified") endif diff --git a/pack/acp/start/vim-go/autoload/go/template.vim b/pack/acp/start/vim-go/autoload/go/template.vim index 3f5e538..404047a 100644 --- a/pack/acp/start/vim-go/autoload/go/template.vim +++ b/pack/acp/start/vim-go/autoload/go/template.vim @@ -24,8 +24,13 @@ function! go#template#create() abort else let l:template_file = go#config#TemplateFile() endif - let l:template_path = go#util#Join(l:root_dir, "templates", l:template_file) - silent exe 'keepalt 0r ' . fnameescape(l:template_path) + " If template_file is an absolute path, use it as-is. This is to support + " overrides pointing to templates outside of the vim-go plugin dir + if fnamemodify(l:template_file, ':p') != l:template_file + let l:template_file = go#util#Join(l:root_dir, "templates", l:template_file) + endif + + silent exe 'keepalt 0r ' . fnameescape(l:template_file) endif else let l:content = printf("package %s", l:package_name) diff --git a/pack/acp/start/vim-go/autoload/go/term.vim b/pack/acp/start/vim-go/autoload/go/term.vim index d7a463e..dca9b50 100644 --- a/pack/acp/start/vim-go/autoload/go/term.vim +++ b/pack/acp/start/vim-go/autoload/go/term.vim @@ -2,6 +2,8 @@ let s:cpo_save = &cpo set cpo&vim +let s:bufnameprefix = 'goterm://' + " new creates a new terminal with the given command. Mode is set based on the " global variable g:go_term_mode, which is by default set to :vsplit function! go#term#new(bang, cmd, errorformat) abort @@ -15,6 +17,10 @@ function! go#term#newmode(bang, cmd, errorformat, mode) abort let l:mode = go#config#TermMode() endif + if go#config#TermReuse() + call s:closeterm() + endif + let l:state = { \ 'cmd': a:cmd, \ 'bang' : a:bang, @@ -24,55 +30,96 @@ function! go#term#newmode(bang, cmd, errorformat, mode) abort \ 'errorformat': a:errorformat, \ } - " execute go build in the files directory - let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' - let l:dir = getcwd() - - execute l:cd . fnameescape(expand("%:p:h")) + " execute the command in the current file's directory + let l:dir = go#util#Chdir(expand('%:p:h')) execute l:mode . ' __go_term__' - setlocal filetype=goterm setlocal bufhidden=delete setlocal winfixheight + " TODO(bc)?: setlocal winfixwidth setlocal noswapfile setlocal nobuflisted - " explicitly bind callbacks to state so that within them, self will always - " refer to state. See :help Partial for more information. - " - " Don't set an on_stderr, because it will be passed the same data as - " on_stdout. See https://github.com/neovim/neovim/issues/2836 - let l:job = { - \ 'on_stdout': function('s:on_stdout', [], state), - \ 'on_exit' : function('s:on_exit', [], state), - \ } + " setup job for nvim + if has('nvim') + " explicitly bind callbacks to state so that within them, self will always + " refer to state. See :help Partial for more information. + " + " Don't set an on_stderr, because it will be passed the same data as + " on_stdout. See https://github.com/neovim/neovim/issues/2836 + let l:job = { + \ 'on_stdout': function('s:on_stdout', [], state), + \ 'on_exit' : function('s:on_exit', [], state), + \ } + let l:state.id = termopen(a:cmd, l:job) + let l:state.termwinid = win_getid(winnr()) + let s:lasttermwinid = l:state.termwinid + call go#util#Chdir(l:dir) - let l:state.id = termopen(a:cmd, l:job) - let l:state.termwinid = win_getid(winnr()) + " resize new term if needed. + let l:height = go#config#TermHeight() + let l:width = go#config#TermWidth() - execute l:cd . fnameescape(l:dir) + " Adjust the window width or height depending on whether it's a vertical or + " horizontal split. + if l:mode =~ "vertical" || l:mode =~ "vsplit" || l:mode =~ "vnew" + exe 'vertical resize ' . l:width + elseif mode =~ "split" || mode =~ "new" + exe 'resize ' . l:height + endif + " we also need to resize the pty, so there you go... + call jobresize(l:state.id, l:width, l:height) - " resize new term if needed. - let l:height = go#config#TermHeight() - let l:width = go#config#TermWidth() + " setup term for vim8 + elseif has('terminal') + " Not great randomness, but "good enough" for our purpose here. + let l:rnd = sha256(printf('%s%s', reltimestr(reltime()), fnamemodify(bufname(''), ":p"))) + let l:termname = printf("%s%s", s:bufnameprefix, l:rnd) - " Adjust the window width or height depending on whether it's a vertical or - " horizontal split. - if l:mode =~ "vertical" || l:mode =~ "vsplit" || l:mode =~ "vnew" - exe 'vertical resize ' . l:width - elseif mode =~ "split" || mode =~ "new" - exe 'resize ' . l:height + let l:term = { + \ 'out_cb': function('s:out_cb', [], state), + \ 'exit_cb' : function('s:exit_cb', [], state), + \ 'curwin': 1, + \ 'term_name': l:termname, + \ } + + if l:mode =~ "vertical" || l:mode =~ "vsplit" || l:mode =~ "vnew" + let l:term["vertical"] = l:mode + endif + + let l:state.id = term_start(a:cmd, l:term) + let l:state.termwinid = win_getid(bufwinnr(l:state.id)) + let s:lasttermwinid = l:state.termwinid + call go#util#Chdir(l:dir) + + " resize new term if needed. + let l:height = go#config#TermHeight() + let l:width = go#config#TermWidth() + + " Adjust the window width or height depending on whether it's a vertical or + " horizontal split. + if l:mode =~ "vertical" || l:mode =~ "vsplit" || l:mode =~ "vnew" + exe 'vertical resize ' . l:width + elseif mode =~ "split" || mode =~ "new" + exe 'resize ' . l:height + endif + "if exists(*term_setsize) + "call term_setsize(l:state.id, l:height, l:width) + "endif endif - " we also need to resize the pty, so there you go... - call jobresize(l:state.id, l:width, l:height) - call win_gotoid(l:state.winid) - return l:state.id endfunction +" out_cb continually concat's the self.stdout_buf on recv of stdout +" and sets self.stdout to the new-lined split content in self.stdout_buf +func! s:out_cb(channel, msg) dict abort + let self.stdout_buf = self.stdout_buf . a:msg + let self.stdout = split(self.stdout_buf, '\n') +endfunction + function! s:on_stdout(job_id, data, event) dict abort " A single empty string means EOF was reached. The first item will never be " the empty string except for when it's the only item and is signaling that @@ -95,9 +142,21 @@ function! s:on_stdout(job_id, data, event) dict abort endif endfunction +" vim8 exit callback +function! s:exit_cb(job_id, exit_status) dict abort + call s:handle_exit(a:job_id, a:exit_status, self) +endfunction + +" nvim exit callback function! s:on_exit(job_id, exit_status, event) dict abort + call s:handle_exit(a:job_id, a:exit_status, self) +endfunction + +" handle_exit implements both vim8 and nvim exit callbacks +func s:handle_exit(job_id, exit_status, state) abort let l:winid = win_getid(winnr()) - call win_gotoid(self.winid) + call win_gotoid(a:state.winid) + let l:listtype = go#list#Type("_term") if a:exit_status == 0 @@ -106,40 +165,106 @@ function! s:on_exit(job_id, exit_status, event) dict abort return endif - call win_gotoid(self.winid) + let l:bufdir = expand('%:p:h') + if !isdirectory(l:bufdir) + call go#util#EchoWarning('terminal job failure not processed, because the job''s working directory no longer exists') + call win_gotoid(l:winid) + return + endif - let l:title = self.cmd + " change to directory where the command was run. If we do not do this the + " quickfix items will have the incorrect paths. + " see: https://github.com/fatih/vim-go/issues/2400 + let l:dir = go#util#Chdir(l:bufdir) + + let l:title = a:state.cmd if type(l:title) == v:t_list - let l:title = join(self.cmd) + let l:title = join(a:state.cmd) endif let l:i = 0 - while l:i < len(self.stdout) - let self.stdout[l:i] = substitute(self.stdout[l:i], "\r$", '', 'g') + while l:i < len(a:state.stdout) + let a:state.stdout[l:i] = substitute(a:state.stdout[l:i], "\r$", '', 'g') let l:i += 1 endwhile - call go#list#ParseFormat(l:listtype, self.errorformat, self.stdout, l:title) + call go#list#ParseFormat(l:listtype, a:state.errorformat, a:state.stdout, l:title, 0) let l:errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(l:errors)) + " close terminal; we don't need it anymore + if go#config#TermCloseOnExit() + call win_gotoid(a:state.termwinid) + close! + endif + if empty(l:errors) call go#util#EchoError( '[' . l:title . '] ' . "FAIL") + call go#util#Chdir(l:dir) call win_gotoid(l:winid) return endif - " close terminal; we don't need it anymore - call win_gotoid(self.termwinid) - close! - - if self.bang + if a:state.bang + call go#util#Chdir(l:dir) call win_gotoid(l:winid) return endif - call win_gotoid(self.winid) + call win_gotoid(a:state.winid) call go#list#JumpToFirst(l:listtype) + + " change back to original working directory + call go#util#Chdir(l:dir) +endfunction + +function! go#term#ToggleCloseOnExit() abort + if go#config#TermCloseOnExit() + call go#config#SetTermCloseOnExit(0) + call go#util#EchoProgress("term close on exit disabled") + return + endif + + call go#config#SetTermCloseOnExit(1) + call go#util#EchoProgress("term close on exit enabled") + return +endfunction + +function! s:closeterm() + if !exists('s:lasttermwinid') + return + endif + + try + let l:termwinid = s:lasttermwinid + unlet s:lasttermwinid + let l:info = getwininfo(l:termwinid) + if empty(l:info) + return + endif + + let l:info = l:info[0] + + if !get(l:info, 'terminal', 0) is 1 + return + endif + + if has('nvim') + if 'goterm' == nvim_buf_get_option(nvim_win_get_buf(l:termwinid), 'filetype') + call nvim_win_close(l:termwinid, v:true) + endif + return + endif + + if stridx(bufname(winbufnr(l:termwinid)), s:bufnameprefix, 0) == 0 + let l:winid = win_getid() + call win_gotoid(l:termwinid) + close! + call win_gotoid(l:winid) + endif + catch + call go#util#EchoError(printf("vim-go: %s", v:exception)) + endtry endfunction " restore Vi compatibility settings diff --git a/pack/acp/start/vim-go/autoload/go/term_test.vim b/pack/acp/start/vim-go/autoload/go/term_test.vim index 40f0a52..55e2e10 100644 --- a/pack/acp/start/vim-go/autoload/go/term_test.vim +++ b/pack/acp/start/vim-go/autoload/go/term_test.vim @@ -3,7 +3,7 @@ let s:cpo_save = &cpo set cpo&vim func! Test_GoTermNewMode() - if !has('nvim') + if !(has('nvim') || has('terminal')) return endif @@ -22,12 +22,13 @@ func! Test_GoTermNewMode() call assert_equal(actual, l:expected) finally + sleep 50m call delete(l:tmp, 'rf') endtry endfunc func! Test_GoTermNewMode_SplitRight() - if !has('nvim') + if !(has('nvim') || has('terminal')) return endif @@ -46,11 +47,46 @@ func! Test_GoTermNewMode_SplitRight() call assert_equal(actual, l:expected) finally + sleep 50m call delete(l:tmp, 'rf') set nosplitright endtry endfunc +func! Test_GoTermReuse() + if !(has('nvim') || has('terminal')) + return + endif + + try + let l:filename = 'term/term.go' + let l:tmp = gotest#load_fixture(l:filename) + exe 'cd ' . l:tmp . '/src/term' + + let expected = expand('%:p') + + let cmd = "go run ". go#util#Shelljoin(go#tool#Files()) + + set nosplitright + + let g:go_term_reuse = 1 + call go#term#new(0, cmd, &errorformat) + let actual = expand('%:p') + call assert_equal(actual, l:expected) + call assert_equal(3, len(getwininfo())) + + call go#term#new(0, cmd, &errorformat) + let actual = expand('%:p') + call assert_equal(actual, l:expected) + + call assert_equal(3, len(getwininfo())) + finally + sleep 50m + unlet g:go_term_reuse + call delete(l:tmp, 'rf') + endtry +endfunc + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/buildtags.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/buildtags.go new file mode 100644 index 0000000..5bf7a88 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/buildtags.go @@ -0,0 +1,5 @@ +package config + +func Example() { + foo() +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/constrainedfoo.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/constrainedfoo.go new file mode 100644 index 0000000..1a7b3f2 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/constrainedfoo.go @@ -0,0 +1,8 @@ +// +build constrained + +package config + +// foo is constrained and this comment exists to make the line numbers different than foo.go +func foo() { + println("foo") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/foo.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/foo.go new file mode 100644 index 0000000..69266c7 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/foo.go @@ -0,0 +1,7 @@ +// +build !constrained + +package config + +func foo() { + println("foo") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/go.mod b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/go.mod new file mode 100644 index 0000000..d930dc7 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/config/buildtags/go.mod @@ -0,0 +1,3 @@ +module config + +go 1.13 diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/goimports.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/goimports.go deleted file mode 100644 index eec47f8..0000000 --- a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/goimports.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "fmt" -) - -func Foo(log *logging.TestLogger) { -log.Debug("vim-go") -} - -func main() { - fmt.Println("vim-go") -} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/goimports_golden.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/goimports_golden.go deleted file mode 100644 index 3719f6b..0000000 --- a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/goimports_golden.go +++ /dev/null @@ -1,15 +0,0 @@ -package main - -import ( - "fmt" - - logging "gh.com/gi/foo-logging" -) - -func Foo(log *logging.TestLogger) { - log.Debug("vim-go") -} - -func main() { - fmt.Println("vim-go") -} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/vendor/gh.com/gi/foo-logging/logger.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/vendor/gh.com/gi/foo-logging/logger.go deleted file mode 100644 index 1396018..0000000 --- a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/imports/vendor/gh.com/gi/foo-logging/logger.go +++ /dev/null @@ -1,12 +0,0 @@ -package logging - -import "fmt" - -type TestLogger struct { - Value string -} - -func (l *TestLogger) Debug(msg string) { - fmt.Println(msg) - fmt.Println(l.Value) -} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/src/imports b/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/src/imports deleted file mode 100644 index 60ee253..0000000 --- a/pack/acp/start/vim-go/autoload/go/test-fixtures/fmt/src/imports +++ /dev/null @@ -1 +0,0 @@ -../imports/ \ No newline at end of file diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/job/dir has spaces/main.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/job/dir has spaces/main.go new file mode 100644 index 0000000..5d31fef --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/job/dir has spaces/main.go @@ -0,0 +1,6 @@ +package main + +func main() { + notafunc() + println("vim-go") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/compilererror/compilererror.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/compilererror/compilererror.go new file mode 100644 index 0000000..3f3dc7c --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/compilererror/compilererror.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("vim-go" +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/errcheck.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/errcheck.go new file mode 100644 index 0000000..72d7fc3 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/errcheck.go @@ -0,0 +1,10 @@ +package errcheck + +import ( + "io" + "os" +) + +func foo() { + io.Copy(os.Stdout, os.Stdin) +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/errcheck_test.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/errcheck_test.go new file mode 100644 index 0000000..445d9e5 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/errcheck/errcheck_test.go @@ -0,0 +1,11 @@ +package errcheck + +import ( + "io" + "os" + "testing" +) + +func TestFoo(t *testing.T) { + io.Copy(os.Stdout, os.Stdin) +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/baz.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/baz.go new file mode 100644 index 0000000..ffff9fe --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/baz.go @@ -0,0 +1,3 @@ +package lint + +func baz() {} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/importabs/ok.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/importabs/ok.go new file mode 100644 index 0000000..8e4d5c6 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/importabs/ok.go @@ -0,0 +1,3 @@ +package problems + +func bar() {} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/importabs/problems.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/importabs/problems.go new file mode 100644 index 0000000..995f3ee --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/importabs/problems.go @@ -0,0 +1,5 @@ +package problems + +import "/quux" + +func baz() {} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/multiple/problems.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/multiple/problems.go new file mode 100644 index 0000000..4ec38e7 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/multiple/problems.go @@ -0,0 +1,9 @@ +package problems + +import ( + "time" +) + +func mySleep(time int) { + time.Sleep(500 * time.Millisecond) +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/shadow/problems.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/shadow/problems.go new file mode 100644 index 0000000..bdd138a --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/lint/golangci-lint/problems/shadow/problems.go @@ -0,0 +1,5 @@ +package problems + +func mySleep(time int) { + time.Sleep(500 * time.Millisecond) +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/vet/compilererror/compilererror.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/vet/compilererror/compilererror.go new file mode 100644 index 0000000..3f3dc7c --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lint/src/vet/compilererror/compilererror.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("vim-go" +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/format.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/format.go new file mode 100644 index 0000000..3be42f6 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/format.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + + func main() { +fmt.Println("vim-go") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/format_golden.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/format_golden.go new file mode 100644 index 0000000..50e8d8d --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/format_golden.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("vim-go") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/newline.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/newline.go new file mode 100644 index 0000000..21948d6 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/fmt/newline.go @@ -0,0 +1,6 @@ +package main + +import "fmt" +func main() { + fmt.Println("vim-go") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/imports/imports.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/imports/imports.go new file mode 100644 index 0000000..f068a3e --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/imports/imports.go @@ -0,0 +1,10 @@ +package main + +import ( + "fmt" +) + +func main() { + io.Copy(ioutil.Discard, os.Stdin) + fmt.Println("vim-go") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/imports/imports_golden.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/imports/imports_golden.go new file mode 100644 index 0000000..5175514 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/lsp/imports/imports_golden.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "io" + "io/ioutil" + "os" +) + +func main() { + io.Copy(ioutil.Discard, os.Stdin) + fmt.Println("vim-go") +} diff --git a/pack/acp/start/vim-go/autoload/go/test-fixtures/test/src/example/example_test.go b/pack/acp/start/vim-go/autoload/go/test-fixtures/test/src/example/example_test.go new file mode 100644 index 0000000..bdcc2e1 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/test-fixtures/test/src/example/example_test.go @@ -0,0 +1,10 @@ +package main + +import ( + "fmt" +) + +func ExampleHelloWorld() { + fmt.Println("Hello, World") + // Output: What's shakin +} diff --git a/pack/acp/start/vim-go/autoload/go/test.vim b/pack/acp/start/vim-go/autoload/go/test.vim index f33d7d9..89dcda6 100644 --- a/pack/acp/start/vim-go/autoload/go/test.vim +++ b/pack/acp/start/vim-go/autoload/go/test.vim @@ -6,7 +6,10 @@ set cpo&vim " compile the tests instead of running them (useful to catch errors in the " test files). Any other argument is appended to the final `go test` command. function! go#test#Test(bang, compile, ...) abort - let args = ["test", '-tags', go#config#BuildTags()] + let args = ["test"] + if len(go#config#BuildTags()) > 0 + call extend(args, ["-tags", go#config#BuildTags()]) + endif " don't run the test, only compile it. Useful to capture and fix errors. if a:compile @@ -32,6 +35,7 @@ function! go#test#Test(bang, compile, ...) abort if go#config#TermEnabled() call go#term#new(a:bang, ["go"] + args, s:errorformat()) + return endif if go#util#has_job() @@ -76,7 +80,7 @@ function! go#test#Test(bang, compile, ...) abort if l:err != 0 let l:winid = win_getid(winnr()) - call go#list#ParseFormat(l:listtype, s:errorformat(), split(out, '\n'), l:cmd) + call go#list#ParseFormat(l:listtype, s:errorformat(), split(out, '\n'), l:cmd, 0) let errors = go#list#Get(l:listtype) call go#list#Window(l:listtype, len(errors)) if empty(errors) @@ -166,9 +170,17 @@ function! s:errorformat() abort let format .= ",%-G" . indent . "%#--- PASS: %.%#" " Match failure lines. - " + + " Example failures start with '--- FAIL: ', followed by the example name + " followed by a space , followed by the duration of the example in + " parantheses. They aren't nested, though, so don't check for indentation. + " The errors from them also aren't indented and don't report file location + " or line numbers, so those won't show up. This will at least let the user + " know which example failed, though. + let format .= ',%G--- FAIL: %\\%(Example%\\)%\\@=%m (%.%#)' + " Test failures start with '--- FAIL: ', followed by the test name followed - " by a space the duration of the test in parentheses + " by a space, followed by the duration of the test in parentheses. " " e.g.: " '--- FAIL: TestSomething (0.00s)' @@ -207,6 +219,14 @@ function! s:errorformat() abort let format .= ",%G" . indent . "%#%\\t%\\{2}%m" " }}}1 + " Go 1.14 test verbose output {{{1 + " Match test output lines similarly to Go 1.11 test output lines, but they + " have the test name followed by a colon before the filename when run with + " the -v flag. + let format .= ",%A" . indent . "%\\+%[%^:]%\\+: %f:%l: %m" + let format .= ",%A" . indent . "%\\+%[%^:]%\\+: %f:%l: " + " }}}1 + " Go 1.11 test output {{{1 " Match test output lines similarly to Go 1.10 test output lines, but they " use an indent level where the Go 1.10 test output uses tabs, so they'll diff --git a/pack/acp/start/vim-go/autoload/go/test_test.vim b/pack/acp/start/vim-go/autoload/go/test_test.vim index 28d7993..322abed 100644 --- a/pack/acp/start/vim-go/autoload/go/test_test.vim +++ b/pack/acp/start/vim-go/autoload/go/test_test.vim @@ -66,9 +66,9 @@ endfunc func! Test_GoTestShowName() abort let expected = [ \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'TestHelloWorld'}, - \ {'lnum': 6, 'bufnr': 7, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'so long'}, + \ {'lnum': 6, 'bufnr': 8, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'so long'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'TestHelloWorld/sub'}, - \ {'lnum': 9, 'bufnr': 7, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'thanks for all the fish'}, + \ {'lnum': 9, 'bufnr': 8, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'thanks for all the fish'}, \ ] let g:go_test_show_name=1 @@ -78,20 +78,27 @@ endfunc func! Test_GoTestVet() abort let expected = [ - \ {'lnum': 6, 'bufnr': 10, 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'Errorf format %v reads arg #1, but call has 0 args'}, + \ {'lnum': 6, 'bufnr': 11, 'col': 2, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'Errorf format %v reads arg #1, but call has 0 args'}, \ ] call s:test('veterror/veterror.go', expected) endfunc func! Test_GoTestTestCompilerError() abort let expected = [ - \ {'lnum': 10, 'bufnr': 8, 'col': 16, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'cannot use r (type struct {}) as type io.Reader in argument to ioutil.ReadAll:'}, + \ {'lnum': 10, 'bufnr': 9, 'col': 16, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'cannot use r (type struct {}) as type io.Reader in argument to ioutil.ReadAll:'}, \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'struct {} does not implement io.Reader (missing Read method)'} \ ] call s:test('testcompilerror/testcompilerror_test.go', expected) endfunc +func! Test_GoTestExample() abort + let expected = [ + \ {'lnum': 0, 'bufnr': 0, 'col': 0, 'valid': 1, 'vcol': 0, 'nr': -1, 'type': '', 'pattern': '', 'text': 'ExampleHelloWorld'} + \ ] + call s:test('example/example_test.go', expected) +endfunc + func! s:test(file, expected, ...) abort let $GOPATH = fnameescape(fnamemodify(getcwd(), ':p')) . 'test-fixtures/test' silent exe 'e ' . $GOPATH . '/src/' . a:file diff --git a/pack/acp/start/vim-go/autoload/go/tool.vim b/pack/acp/start/vim-go/autoload/go/tool.vim index fdeaaf1..fccf433 100644 --- a/pack/acp/start/vim-go/autoload/go/tool.vim +++ b/pack/acp/start/vim-go/autoload/go/tool.vim @@ -82,14 +82,16 @@ endfunction function! go#tool#Info(showstatus) abort let l:mode = go#config#InfoMode() - if l:mode == 'gocode' - call go#complete#Info(a:showstatus) - elseif l:mode == 'guru' + if l:mode == 'guru' call go#guru#DescribeInfo(a:showstatus) elseif l:mode == 'gopls' + if !go#config#GoplsEnabled() + call go#util#EchoError("go_info_mode is 'gopls', but gopls is disabled") + return + endif call go#lsp#Info(a:showstatus) else - call go#util#EchoError('go_info_mode value: '. l:mode .' is not valid. Valid values are: [gocode, guru, gopls]') + call go#util#EchoError('go_info_mode value: '. l:mode .' is not valid. Valid values are: [guru, gopls]') endif endfunction @@ -115,7 +117,15 @@ endfunction function! go#tool#DescribeBalloon() let l:fname = fnamemodify(bufname(v:beval_bufnr), ':p') - call go#lsp#Hover(l:fname, v:beval_lnum, v:beval_col, funcref('s:balloon', [])) + + let l:winid = win_getid() + + call win_gotoid(bufwinid(v:beval_bufnr)) + + let [l:line, l:col] = go#lsp#lsp#Position(v:beval_lnum, v:beval_col) + call go#lsp#Hover(l:fname, l:line, l:col, funcref('s:balloon', [])) + + call win_gotoid(l:winid) return '' endfunction diff --git a/pack/acp/start/vim-go/autoload/go/uri.vim b/pack/acp/start/vim-go/autoload/go/uri.vim index e8f38ab..74d72fb 100644 --- a/pack/acp/start/vim-go/autoload/go/uri.vim +++ b/pack/acp/start/vim-go/autoload/go/uri.vim @@ -18,7 +18,7 @@ function! s:encode(value, unreserved) return substitute( \ a:value, \ a:unreserved, - \ '\="%".printf(''%02X'', char2nr(submatch(0)))', + \ '\=s:encodechar(submatch(0))', \ 'g' \) endfunction @@ -27,10 +27,27 @@ function! go#uri#Decode(value) abort return substitute( \ a:value, \ '%\(\x\x\)', - \ '\=nr2char(''0X'' . submatch(1))', + \ '\=s:decodehex(submatch(1))', \ 'g' \) endfunction + +function! s:encodechar(value) + let l:idx = 0 + let l:encoded = '' + while l:idx < strlen(a:value) + let l:byte = strpart(a:value, l:idx, 1) + let l:encoded = printf('%s%%%02X', l:encoded, char2nr(l:byte)) + let l:idx += 1 + endwhile + + return l:encoded +endfunction + +function! s:decodehex(value) + return eval(printf('"\x%s"', a:value)) +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/go/uri_test.vim b/pack/acp/start/vim-go/autoload/go/uri_test.vim new file mode 100644 index 0000000..279f045 --- /dev/null +++ b/pack/acp/start/vim-go/autoload/go/uri_test.vim @@ -0,0 +1,56 @@ +" don't spam the user when Vim is started in Vi compatibility mode +let s:cpo_save = &cpo +set cpo&vim + +scriptencoding utf-8 + +func! Test_EncodePath_simple() abort + let l:uri = '/simple/foo' + let l:expected = '/simple/foo' + + let l:actual = go#uri#EncodePath(l:uri) + call assert_equal(l:expected, l:actual) +endfunc + +func! Test_EncodePath_multibyte() abort + let l:uri = '/multi-byte/⌘⌘' + let l:expected = '/multi-byte/%E2%8C%98%E2%8C%98' + + let l:actual = go#uri#EncodePath(l:uri) + call assert_equal(l:expected, l:actual) +endfunc + +func! Test_Decode_simple() abort + let l:uri = '/simple/foo' + let l:expected = '/simple/foo' + + let l:actual = go#uri#Decode(l:uri) + call assert_equal(l:expected, l:actual) +endfunc + +func! Test_Decode_multibyte() abort + let l:uri = '/multi-byte/%E2%8C%98%E2%8C%98' + let l:expected = '/multi-byte/⌘⌘' + let l:actual = go#uri#Decode(l:uri) + call assert_equal(l:expected, l:actual) +endfunc + +func! Test_Roundtrip_simple() abort + let l:expected = '/simple/foo' + + let l:actual = go#uri#Decode(go#uri#EncodePath(l:expected)) + call assert_equal(l:expected, l:actual) +endfunc + +func! Test_Roundtrip_multibyte() abort + let l:expected = '/multi-byte/⌘⌘' + + let l:actual = go#uri#Decode(go#uri#EncodePath(l:expected)) + call assert_equal(l:expected, l:actual) +endfunc + +" restore Vi compatibility settings +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/autoload/go/util.vim b/pack/acp/start/vim-go/autoload/go/util.vim index bccb934..d1cc48d 100644 --- a/pack/acp/start/vim-go/autoload/go/util.vim +++ b/pack/acp/start/vim-go/autoload/go/util.vim @@ -36,15 +36,9 @@ function! go#util#Join(...) abort endfunction " IsWin returns 1 if current OS is Windows or 0 otherwise +" Note that has('win32') is always 1 when has('win64') is 1, so has('win32') is enough. function! go#util#IsWin() abort - let win = ['win16', 'win32', 'win64', 'win95'] - for w in win - if (has(w)) - return 1 - endif - endfor - - return 0 + return has('win32') endfunction " IsMac returns 1 if current OS is macOS or 0 otherwise. @@ -68,18 +62,7 @@ endfunction " The (optional) first parameter can be added to indicate the 'cwd' or 'env' " parameters will be used, which wasn't added until a later version. function! go#util#has_job(...) abort - if has('nvim') - return 1 - endif - - " cwd and env parameters to job_start was added in this version. - if a:0 > 0 && a:1 is 1 - return has('job') && has("patch-8.0.0902") - endif - - " job was introduced in 7.4.xxx however there are multiple bug fixes and one - " of the latest is 8.0.0087 which is required for a stable async API. - return has('job') && has("patch-8.0.0087") + return has('job') || has('nvim') endfunction let s:env_cache = {} @@ -137,9 +120,39 @@ function! go#util#gomod() abort return substitute(s:exec(['go', 'env', 'GOMOD'])[0], '\n', '', 'g') endfunction +" gomodcache returns 'go env GOMODCACHE'. Instead use 'go#util#env("gomodcache")' +function! go#util#gomodcache() abort + return substitute(s:exec(['go', 'env', 'GOMODCACHE'])[0], '\n', '', 'g') +endfunction -function! go#util#osarch() abort - return go#util#env("goos") . '_' . go#util#env("goarch") +" hostosarch returns the OS and ARCH values that the go binary is intended for. +function! go#util#hostosarch() abort + let [l:hostos, l:err] = s:exec(['go', 'env', 'GOHOSTOS']) + let [l:hostarch, l:err] = s:exec(['go', 'env', 'GOHOSTARCH']) + return [substitute(l:hostos, '\n', '', 'g'), substitute(l:hostarch, '\n', '', 'g')] +endfunction + +" go#util#ModuleRoot returns the root directory of the module of the current +" buffer. +function! go#util#ModuleRoot() abort + let [l:out, l:err] = go#util#ExecInDir(['go', 'env', 'GOMOD']) + if l:err != 0 + return -1 + endif + + let l:module = split(l:out, '\n', 1)[0] + + " When run with `GO111MODULE=on and not in a module directory, the module will be reported as /dev/null. + let l:fakeModule = '/dev/null' + if go#util#IsWin() + let l:fakeModule = 'NUL' + endif + + if l:fakeModule == l:module + return expand('%:p:h') + endif + + return resolve(fnamemodify(l:module, ':p:h')) endfunction " Run a shell command. @@ -157,6 +170,13 @@ function! s:system(cmd, ...) abort set shell=/bin/sh shellredir=>%s\ 2>&1 shellcmdflag=-c endif + if go#util#IsWin() + if executable($COMSPEC) + let &shell = $COMSPEC + set shellcmdflag=/C + endif + endif + try return call('system', [a:cmd] + a:000) finally @@ -196,18 +216,25 @@ function! go#util#Exec(cmd, ...) abort return call('s:exec', [[l:bin] + a:cmd[1:]] + a:000) endfunction +" ExecInDir will execute cmd with the working directory set to the current +" buffer's directory. function! go#util#ExecInDir(cmd, ...) abort - if !isdirectory(expand("%:p:h")) + let l:wd = expand('%:p:h') + return call('go#util#ExecInWorkDir', [a:cmd, l:wd] + a:000) +endfunction + +" ExecInWorkDir will execute cmd with the working diretory set to wd. Additional arguments will be passed +" to cmd. +function! go#util#ExecInWorkDir(cmd, wd, ...) abort + if !isdirectory(a:wd) return ['', 1] endif - let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' - let dir = getcwd() + let l:dir = go#util#Chdir(a:wd) try - execute cd . fnameescape(expand("%:p:h")) let [l:out, l:err] = call('go#util#Exec', [a:cmd] + a:000) finally - execute cd . fnameescape(l:dir) + call go#util#Chdir(l:dir) endtry return [l:out, l:err] endfunction @@ -446,7 +473,7 @@ function! go#util#tempdir(prefix) abort endif " Not great randomness, but "good enough" for our purpose here. - let l:rnd = sha256(printf('%s%s', localtime(), fnamemodify(bufname(''), ":p"))) + let l:rnd = sha256(printf('%s%s', reltimestr(reltime()), fnamemodify(bufname(''), ":p"))) let l:tmp = printf("%s/%s%s", l:dir, a:prefix, l:rnd) call mkdir(l:tmp, 'p', 0700) return l:tmp @@ -519,6 +546,170 @@ function! go#util#ShowInfo(info) echo "vim-go: " | echohl Function | echon a:info | echohl None endfunction +" go#util#SetEnv takes the name of an environment variable and what its value +" should be and returns a function that will restore it to its original value. +function! go#util#SetEnv(name, value) abort + let l:state = {} + + if len(a:name) == 0 + return function('s:noop', [], l:state) + endif + + let l:remove = 0 + if exists('$' . a:name) + let l:oldvalue = eval('$' . a:name) + else + let l:remove = 1 + endif + + " wrap the value in single quotes so that it will work on windows when there + " are backslashes present in the value (e.g. $PATH). + call execute('let $' . a:name . " = '" . a:value . "'") + + if l:remove + return function('s:unset', [a:name], l:state) + endif + + return function('go#util#SetEnv', [a:name, l:oldvalue], l:state) +endfunction + +function! go#util#ClearHighlights(group) abort + if has('textprop') + " the property type may not exist when syntax highlighting is not enabled. + if empty(prop_type_get(a:group)) + return + endif + if !has('patch-8.1.1035') + return prop_remove({'type': a:group, 'all': 1}, 1, line('$')) + endif + return prop_remove({'type': a:group, 'all': 1}) + endif + + if exists("*matchaddpos") + return s:clear_group_from_matches(a:group) + endif +endfunction + +function! s:clear_group_from_matches(group) abort + let l:cleared = 0 + + let m = getmatches() + for item in m + if item['group'] == a:group + call matchdelete(item['id']) + let l:cleared = 1 + endif + endfor + + return l:cleared +endfunction + +function! s:unset(name) abort + try + " unlet $VAR was introducted in Vim 8.0.1832, which is newer than the + " minimal version that vim-go supports. Set the environment variable to + " the empty string in that case. It's not perfect, but it will work fine + " for most things, and is really the best alternative that's available. + if !has('patch-8.0.1832') + call go#util#SetEnv(a:name, '') + return + endif + + call execute('unlet $' . a:name) + catch + call go#util#EchoError(printf('could not unset $%s: %s', a:name, v:exception)) + endtry +endfunction + +function! s:noop(...) abort dict +endfunction + +" go#util#HighlightPositions highlights using text properties if possible and +" falls back to matchaddpos() if necessary. It works around matchaddpos()'s +" limit of only 8 positions per call by calling matchaddpos() with no more +" than 8 positions per call. +" +" pos should be a list of 3 element lists. The lists should be [line, col, +" length] as used by matchaddpos(). +function! go#util#HighlightPositions(group, pos) abort + if has('textprop') + for l:pos in a:pos + " use a single line prop by default + let l:prop = {'type': a:group, 'length': l:pos[2]} + + let l:line = getline(l:pos[0]) + + " l:max is the 1-based index within the buffer of the first character after l:pos. + let l:max = line2byte(l:pos[0]) + l:pos[1] + l:pos[2] - 1 + if has('patch-8.2.115') + " Use byte2line as long as 8.2.115 (which resolved + " https://github.com/vim/vim/issues/5334) is available. + let l:end_lnum = byte2line(l:max) + + " specify end line and column if needed. + if l:pos[0] != l:end_lnum + let l:end_col = l:max - line2byte(l:end_lnum) + let l:prop = {'type': a:group, 'end_lnum': l:end_lnum, 'end_col': l:end_col} + endif + elseif l:pos[1] + l:pos[2] - 1 > len(l:line) + let l:end_lnum = l:pos[0] + while line2byte(l:end_lnum+1) < l:max + let l:end_lnum += 1 + endwhile + + " l:end_col is the full length - the byte position of l:end_lnum + + " the number of newlines (number of newlines is l:end_lnum - + " l:pos[0]. + let l:end_col = l:max - line2byte(l:end_lnum) + l:end_lnum - l:pos[0] + let l:prop = {'type': a:group, 'end_lnum': l:end_lnum, 'end_col': l:end_col} + endif + try + call prop_add(l:pos[0], l:pos[1], l:prop) + catch + " Swallow any exceptions encountered while trying to add the property + " Due to the asynchronous nature, it's possible that the buffer has + " changed since the buffer was analyzed and that the specified + " position is no longer valid. + endtry + endfor + return + endif + + if exists('*matchaddpos') + return s:matchaddpos(a:group, a:pos) + endif +endfunction + +" s:matchaddpos works around matchaddpos()'s limit of only 8 positions per +" call by calling matchaddpos() with no more than 8 positions per call. +function! s:matchaddpos(group, pos) abort + let l:partitions = [] + let l:partitionsIdx = 0 + let l:posIdx = 0 + for l:pos in a:pos + if l:posIdx % 8 == 0 + let l:partitions = add(l:partitions, []) + let l:partitionsIdx = len(l:partitions) - 1 + endif + let l:partitions[l:partitionsIdx] = add(l:partitions[l:partitionsIdx], l:pos) + let l:posIdx = l:posIdx + 1 + endfor + + for l:positions in l:partitions + call matchaddpos(a:group, l:positions) + endfor +endfunction + +function! go#util#Chdir(dir) abort + if !exists('*chdir') + let l:olddir = getcwd() + let cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' + execute cd . fnameescape(a:dir) + return l:olddir + endif + return chdir(a:dir) +endfunction + " restore Vi compatibility settings let &cpo = s:cpo_save unlet s:cpo_save diff --git a/pack/acp/start/vim-go/autoload/gotest.vim b/pack/acp/start/vim-go/autoload/gotest.vim index e010d52..4ee727a 100644 --- a/pack/acp/start/vim-go/autoload/gotest.vim +++ b/pack/acp/start/vim-go/autoload/gotest.vim @@ -18,16 +18,23 @@ fun! gotest#write_file(path, contents) abort call mkdir(fnamemodify(l:full_path, ':h'), 'p') call writefile(a:contents, l:full_path) - exe 'cd ' . l:dir . '/src' + call go#util#Chdir(l:dir . '/src') + silent exe 'e! ' . a:path " Set cursor. let l:lnum = 1 for l:line in a:contents - let l:m = match(l:line, "\x1f") + let l:m = stridx(l:line, "\x1f") if l:m > -1 - call setpos('.', [0, l:lnum, l:m, 0]) + let l:byte = line2byte(l:lnum) + l:m + exe 'goto '. l:byte call setline('.', substitute(getline('.'), "\x1f", '', '')) + silent noautocmd w! + + call go#lsp#DidClose(expand('%:p')) + call go#lsp#DidOpen(expand('%:p')) + break endif @@ -42,15 +49,21 @@ endfun " The file will be copied to a new GOPATH-compliant temporary directory and " loaded as the current buffer. fun! gotest#load_fixture(path) abort + if go#util#has_job() + call go#lsp#CleanWorkspaces() + endif let l:dir = go#util#tempdir("vim-go-test/testrun/") let $GOPATH .= ':' . l:dir let l:full_path = l:dir . '/src/' . a:path call mkdir(fnamemodify(l:full_path, ':h'), 'p') - exe 'cd ' . l:dir . '/src' - silent exe 'noautocmd e ' . a:path + call go#util#Chdir(l:dir . '/src') + silent exe 'noautocmd e! ' . a:path silent exe printf('read %s/test-fixtures/%s', g:vim_go_root, a:path) silent noautocmd w! + if go#util#has_job() + call go#lsp#AddWorkspaceDirectory(fnamemodify(l:full_path, ':p:h')) + endif return l:dir endfun @@ -109,26 +122,29 @@ endfun func! gotest#assert_quickfix(got, want) abort call assert_equal(len(a:want), len(a:got), "number of errors") if len(a:want) != len(a:got) - call assert_equal(a:want, a:got) - return + return assert_equal(a:want, a:got) endif + let l:retval = 0 let i = 0 + while i < len(a:want) let want_item = a:want[i] let got_item = a:got[i] let i += 1 - call assert_equal(want_item.bufnr, got_item.bufnr, "bufnr") - call assert_equal(want_item.lnum, got_item.lnum, "lnum") - call assert_equal(want_item.col, got_item.col, "col") - call assert_equal(want_item.vcol, got_item.vcol, "vcol") - call assert_equal(want_item.nr, got_item.nr, "nr") - call assert_equal(want_item.pattern, got_item.pattern, "pattern") - call assert_equal(want_item.text, got_item.text, "text") - call assert_equal(want_item.type, got_item.type, "type") - call assert_equal(want_item.valid, got_item.valid, "valid") + let l:retval = assert_equal(want_item.bufnr, got_item.bufnr, "bufnr") || l:retval + let l:retval = assert_equal(want_item.lnum, got_item.lnum, "lnum") || l:retval + let l:retval = assert_equal(want_item.col, got_item.col, "col") || l:retval + let l:retval = assert_equal(want_item.vcol, got_item.vcol, "vcol") || l:retval + let l:retval = assert_equal(want_item.nr, got_item.nr, "nr") || l:retval + let l:retval = assert_equal(want_item.pattern, got_item.pattern, "pattern") || l:retval + let l:retval = assert_equal(want_item.text, got_item.text, "text") || l:retval + let l:retval = assert_equal(want_item.type, got_item.type, "type") || l:retval + let l:retval = assert_equal(want_item.valid, got_item.valid, "valid") || l:retval endwhile + + return l:retval endfunc " restore Vi compatibility settings diff --git a/pack/acp/start/vim-go/compiler/go.vim b/pack/acp/start/vim-go/compiler/go.vim index 5f978bb..0bd6a15 100644 --- a/pack/acp/start/vim-go/compiler/go.vim +++ b/pack/acp/start/vim-go/compiler/go.vim @@ -31,14 +31,13 @@ endif " use a different output, for those we define them directly and modify the " errorformat ourselves. More information at: " http://vimdoc.sourceforge.net/htmldoc/quickfix.html#errorformat -CompilerSet errorformat =%-G#\ %.%# " Ignore lines beginning with '#' ('# command-line-arguments' line sometimes appears?) -CompilerSet errorformat+=%-G%.%#panic:\ %m " Ignore lines containing 'panic: message' -CompilerSet errorformat+=%Ecan\'t\ load\ package:\ %m " Start of multiline error string is 'can\'t load package' -CompilerSet errorformat+=%A%f:%l:%c:\ %m " Start of multiline unspecified string is 'filename:linenumber:columnnumber:' -CompilerSet errorformat+=%A%f:%l:\ %m " Start of multiline unspecified string is 'filename:linenumber:' -CompilerSet errorformat+=%C%*\\s%m " Continuation of multiline error message is indented -CompilerSet errorformat+=%-G%.%# " All lines not matching any of the above patterns are ignored - +CompilerSet errorformat =%-G#\ %.%# " Ignore lines beginning with '#' ('# command-line-arguments' line sometimes appears?) +CompilerSet errorformat+=%-G%.%#panic:\ %m " Ignore lines containing 'panic: message' +CompilerSet errorformat+=%Ecan\'t\ load\ package:\ %m " Start of multiline error string is 'can\'t load package' +CompilerSet errorformat+=%A%\\%%(%[%^:]%\\+:\ %\\)%\\?%f:%l:%c:\ %m " Start of multiline unspecified string is 'filename:linenumber:columnnumber:' +CompilerSet errorformat+=%A%\\%%(%[%^:]%\\+:\ %\\)%\\?%f:%l:\ %m " Start of multiline unspecified string is 'filename:linenumber:' +CompilerSet errorformat+=%C%*\\s%m " Continuation of multiline error message is indented +CompilerSet errorformat+=%-G%.%# " All lines not matching any of the above patterns are ignored let &cpo = s:save_cpo unlet s:save_cpo diff --git a/pack/acp/start/vim-go/doc/vim-go.txt b/pack/acp/start/vim-go/doc/vim-go.txt index 3dd3798..14e8062 100644 --- a/pack/acp/start/vim-go/doc/vim-go.txt +++ b/pack/acp/start/vim-go/doc/vim-go.txt @@ -32,17 +32,18 @@ CONTENTS *go-contents* INTRO *go-intro* Go (golang) support for Vim. vim-go comes with sensible predefined settings -(e.g. automatic `gofmt` on save), has autocomplete, snippet support, improved -syntax highlighting, go toolchain commands, etc. It is highly customizable, -and individual features can be toggled easily. vim-go leverages a number of -tools developed by the Go community to provide a seamless Vim experience. +(e.g. automatic `gofmt` on save), has code completion, snippet support, +improved syntax highlighting, go toolchain commands, etc. It is highly +customizable, and individual features can be toggled easily. vim-go leverages +a number of tools developed by the Go community to provide a seamless Vim +experience. * Compile your package with |:GoBuild|, install it with |:GoInstall| or test it with |:GoTest|. Run a single test with |:GoTestFunc|). * Quickly execute your current file(s) with |:GoRun|. * Improved syntax highlighting and folding. * Debug programs with integrated `delve` support with |:GoDebugStart|. - * Completion support via `gocode` and `gopls`. + * Code completion support via `gocode` and `gopls`. * `gofmt` or `goimports` on save keeps the cursor position and undo history. * Go to symbol/declaration with |:GoDef|. * Look up documentation with |:GoDoc| or |:GoDocBrowser|. @@ -50,7 +51,7 @@ tools developed by the Go community to provide a seamless Vim experience. * Precise type-safe renaming of identifiers with |:GoRename|. * See which code is covered by tests with |:GoCoverage|. * Add or remove tags on struct fields with |:GoAddTags| and |:GoRemoveTags|. - * Call `gometalinter` with |:GoMetaLinter| to invoke all possible linters + * Call `golangci-lint` with |:GoMetaLinter| to invoke all possible linters (`golint`, `vet`, `errcheck`, `deadcode`, etc.) and put the result in the quickfix or location list. * Lint your code with |:GoLint|, run your code through |:GoVet| to catch @@ -76,7 +77,7 @@ tools developed by the Go community to provide a seamless Vim experience. ============================================================================== INSTALL *go-install* -vim-go requires at least Vim 7.4.2009 or Neovim 0.3.1. On macOS, if you are +vim-go requires at least Vim 8.0.1453 or Neovim 0.4.0. On macOS, if you are still using your system version of vim, you can use homebrew to keep your version of Vim up-to-date with the following terminal command: > @@ -93,26 +94,26 @@ For Pathogen or Vim |packages|, just clone the repo. For other plugin managers you may also need to add the lines to your vimrc to execute the plugin manager's install command. -* Vim 8 |packages| -> +* Vim 8 |packages| > + git clone https://github.com/fatih/vim-go.git \ ~/.vim/pack/plugins/start/vim-go - +< * https://github.com/tpope/vim-pathogen > git clone https://github.com/fatih/vim-go.git ~/.vim/bundle/vim-go < * https://github.com/junegunn/vim-plug > - Plug 'fatih/vim-go' - + Plug 'fatih/vim-go', { 'do': ':GoUpdateBinaries' } +< * https://github.com/Shougo/neobundle.vim > NeoBundle 'fatih/vim-go' < * https://github.com/gmarik/vundle > - Plugin 'fatih/vim-go', { 'do': ':GoUpdateBinaries' } + Plugin 'fatih/vim-go' < * Manual (not recommended) > @@ -127,7 +128,7 @@ or $GOPATH/bin (default: $HOME/go/bin). It requires `git`. Depending on your installation method, you may have to generate the plugin's |:helptags| manually (e.g. `:helptags ALL`). -Autocompletion is enabled by default via 'omnifunc', which you can trigger +Code completion is enabled by default via 'omnifunc', which you can trigger with |i_CTRL-X_CTRL-O| (``). Supported Go plugins~ *vim-go-plugins* @@ -215,7 +216,7 @@ COMMANDS *go-commands* Open the relevant GoDoc in browser for either the word[s] passed to the command or by default, the word under the cursor. By default it opens the - documentation in 'https://godoc.org'. To change it see |'g:go_doc_url'|. + documentation in 'https://pkg.go.dev'. To change it see |'g:go_doc_url'|. *:GoFmt* :GoFmt @@ -248,9 +249,7 @@ COMMANDS *go-commands* not guarantee all reports are genuine problems, but it can find errors not caught by the compilers. - You may optionally pass any valid go tool vet flags/options. In this case, - `go tool vet` is run in place of `go vet`. For a full list please see - `go tool vet -h`. + You may optionally pass any valid go vet flags/options. If [!] is not given the first error is jumped to. @@ -344,6 +343,9 @@ CTRL-t If using neovim then `:GoRun` will run in a new terminal according to |'g:go_term_mode'|. + The working directory will be the directory containing the current buffer. + + *:GoBuild* :GoBuild[!] [expand] @@ -375,8 +377,8 @@ CTRL-t :GoInfo Show type information about the identifier under the cursor. For example putting it above a function call is going to show the full function - signature. By default it uses `gocode` to get the type informations. To - change the underlying tool from `gocode` to another tool, see + signature. By default it uses `gopls` to get the type informations. To + change the underlying tool from `gopls` to another tool, see |'g:go_info_mode'|. @@ -400,7 +402,7 @@ CTRL-t You may optionally pass any valid go test flags/options. For a full list please see `go help test`. - GoTest timesout automatically after 10 seconds. To customize the timeout + GoTest times out automatically after 10 seconds. To customize the timeout use |'g:go_test_timeout'|. This feature is disabled if any arguments are passed to the `:GoTest` command. @@ -503,7 +505,7 @@ CTRL-t :GoInstallBinaries [binaries] Download and install all necessary Go tool binaries such as `godef`, - `goimports`, `gocode`, etc. under |'g:go_bin_path'|. If [binaries] is + `goimports`, `gopls`, etc. under |'g:go_bin_path'|. If [binaries] is supplied, then only the specified binaries will be installed. The default is to install everything. @@ -513,7 +515,7 @@ CTRL-t :GoUpdateBinaries [binaries] Download and update previously installed Go tool binaries such as `godef`, - `goimports`, `gocode`, etc. under |'g:go_bin_path'|. If [binaries] is + `goimports`, `gopls`, etc. under |'g:go_bin_path'|. If [binaries] is supplied, then only the specified binaries will be updated. The default is to update everything. @@ -535,7 +537,7 @@ CTRL-t *:GoGuruScope* -:GoGuruScope [pattern] [pattern2] ... [patternN] +:GoGuruScope [pattern] ... Changes the custom |'g:go_guru_scope'| setting and overrides it with the given package patterns. The custom scope is cleared (unset) if `""` is @@ -621,8 +623,7 @@ CTRL-t :GoReferrers The referrers query shows the set of identifiers that refer to the same - object as does the selected identifier, within any package in the analysis - scope. + object as does the selected identifier. *:GoSameIds* :GoSameIds @@ -647,19 +648,31 @@ CTRL-t the cursor. This basically toggles the option |'g:go_auto_sameids'| on/off. If enabled it starts highlighting whenever your cursor is staying at the - same position for a configurable period of time (see 'updatetime'). If - disabled it clears and stops automatic highlighting. + same position for a configurable period of time (see |'g:go_updatetime'|). + If disabled it clears and stops automatic highlighting. *:GoMetaLinter* :GoMetaLinter! [path] - Calls the underlying `gometalinter` tool and displays all warnings and + Calls the underlying `golangci-lint` tool and displays all warnings and errors in the |quickfix| window. By default the following linters are enabled: `vet`, `golint`, and `errcheck`. This can be changed with the |'g:go_metalinter_enabled'| variable. To override the command completely use the variable |'g:go_metalinter_command'|. To override the maximum linters execution time use |'g:go_metalinter_deadline'| variable. + If [!] is not given the first error is jumped to. + + *:GoDiagnostics* +:GoDiagnostics! [packages] + + Displays the diagnostics from `gopls` for the given packages in a + |quickfix| window. The diagnostics for the current package are displayed + when no package is given. The diagnostics for all packages will be + displayed when `all` is as an argument. + + Disabled when |'g:go_diagnostics_enabled'| is not set. + If [!] is not given the first error is jumped to. *:GoBuildTags* @@ -667,8 +680,8 @@ CTRL-t Changes the build tags for various commands. If you have any file that uses a custom build tag, such as `// +build integration` , this command - can be used to pass it to all tools that accepts tags, such as guru, - gorename, etc. + can be used to pass it to all tools that accepts tags, such as gopls, + guru, gorename, etc. The build tags is cleared (unset) if `""` is given. If no arguments are given it prints the current build tags. @@ -787,7 +800,7 @@ CTRL-t You can define a constant value instead of the default field based value. For example the following command will add ``valid:"1"`` to all fields. > - :GoAddTags valid=1 + :GoAddTags valid:1 < *:GoRemoveTags* :[range]GoRemoveTags [key],[option] [key1],[option1] ... @@ -834,7 +847,7 @@ CTRL-t Toggles |'g:go_metalinter_autosave'|. - By default, `gometalinter` messages will be shown in the |location-list| + By default, `golangci-lint` messages will be shown in the |location-list| window. The list to use can be set using |'g:go_list_type_commands'|. *:GoTemplateAutoCreateToggle* @@ -905,6 +918,16 @@ CTRL-t tries to preserve cursor position and avoids replacing the buffer with stderr output. + *:GoAddWorkspace* +:GoAddWorkspace [dir] ... + + Add directories to the `gopls` workspace. + + *:GoLSPDebugBrowser* +:GoLSPDebugBrowser + + Open a browser to see gopls debugging information. + ============================================================================== MAPPINGS *go-mappings* @@ -1028,7 +1051,7 @@ Show the relevant GoDoc for the word under in browser *(go-def)* -Goto declaration/definition. Results are shown in the current buffer. +Goto declaration/definition. Results are shown in the current window. *(go-def-split)* @@ -1043,6 +1066,24 @@ Jumps to an existing buffer if |'g:go_def_reuse_buffer'| is enabled. *(go-def-tab)* Goto declaration/definition. Results are shown in a tab window. +Jumps to an existing buffer if |'g:go_def_reuse_buffer'| is enabled. + + *(go-def-type)* + +Goto type declaration/definition. Results are shown in the current window. +Jumps to an existing buffer if |'g:go_def_reuse_buffer'| is enabled. + + *(go-def-type-vertical)* +Goto type declaration/definition. Results are shown in a vertical split +window. +Jumps to an existing buffer if |'g:go_def_reuse_buffer'| is enabled. + + *(go-def-type-split)* +Goto type declaration/definition. Results are shown in a split window. +Jumps to an existing buffer if |'g:go_def_reuse_buffer'| is enabled. + + *(go-def-type-tab)* +Goto type declaration/definition. Results are shown in a tab window. Jumps to an existing buffer if |'g:go_def_reuse_buffer'| is enabled. *(go-def-stack)* @@ -1126,6 +1167,9 @@ return values and the numbers. Calls |:GoModFmt| for the current buffer + *(go-diagnostics)* +Calls `:GoDiagnostics` + ============================================================================== TEXT OBJECTS *go-text-objects* @@ -1187,13 +1231,7 @@ into the statusline. *go#complete#Complete()* -Uses `gopls` for autocompletion. By default, it is hooked up to |'omnifunc'| -for Vim8 and Neovim. - - *go#complete#GocodeComplete()* - -Uses `gocode` for autocompletion. By default, it is hooked up to |'omnifunc'| -for Vim 7.4. +Uses `gopls` for autocompletion. By default, it is hooked up to 'omnifunc'. *go#tool#DescribeBalloon()* @@ -1203,6 +1241,29 @@ balloonexpr`. ============================================================================== SETTINGS *go-settings* + *'g:go_version_warning'* + +Enable warning when using an unsupported version of Vim. By default it is +enabled. +> + let g:go_version_warning = 1 +< + + *'g:go_code_completion_enabled'* + +Enable code completion with 'omnifunc'. By default it is enabled. +> + let g:go_code_completion_enabled = 1 +< + + *'g:go_code_completion_icase'* + +Override the icase field in 'omnifunc' results. By default it is set to 0. +See 'complete-items' for details. +> + let g:go_code_completion_icase = 0 +< + *'g:go_test_show_name'* Show the name of each failed test before the errors and logs output by the @@ -1220,9 +1281,9 @@ set to 10 seconds . > < *'g:go_play_browser_command'* -Browser to use for |:GoPlay| or |:GoDocBrowser|. The url must be added with -`%URL%`, and it's advisable to include `&` to make sure the shell returns. For -example: +Browser to use for |:GoPlay|, |:GoDocBrowser|, and |:GoLSPDebugBrowser|. The +url must be added with `%URL%`, and it's advisable to include `&` to make sure +the shell returns. For example: > let g:go_play_browser_command = 'firefox-developer %URL% &' < @@ -1243,7 +1304,7 @@ with |:GoPlay|. By default it's enabled. > Use this option to show the type info (|:GoInfo|) for the word under the cursor automatically. Whenever the cursor changes the type info will be updated. By default it's disabled. The delay can be configured with the -'g:go_updatetime' setting. +|'g:go_updatetime'| setting. > let g:go_auto_type_info = 0 < @@ -1251,48 +1312,54 @@ updated. By default it's disabled. The delay can be configured with the *'g:go_info_mode'* Use this option to define the command to be used for |:GoInfo|. By default -`gocode` is being used as it's the fastest option. But one might also use -`gopls` or `guru` as they cover more cases and are more accurate. Current -valid options are: `[gocode, guru, gopls]` > - - let g:go_info_mode = 'gocode' +`gopls` is used, because it is the fastest and is known to be highly accurate. +One might also use `guru` for its accuracy. +Valid options are `gopls` and `guru`. +> + let g:go_info_mode = 'gopls' < *'g:go_auto_sameids'* Use this option to highlight all uses of the identifier under the cursor -(:GoSameIds) automatically. By default it's disabled. The delay can be -configured with the 'g:go_updatetime' setting. +(|:GoSameIds|) automatically. By default it's disabled. The delay can be +configured with the |'g:go_updatetime'| setting. > let g:go_auto_sameids = 0 < *'g:go_updatetime'* -Use this option to configure the a custom 'updatetime' for Go source files. If -set to 0, no custom time will be configured. By default it's set to 800ms. +Use this option to configure the delay until it starts some jobs (see +|'g:go_auto_type_info'|, |'g:go_auto_sameids'|). If set to 0, it uses the +value from 'updatetime'. By default it's set to 800ms. > let g:go_updatetime = 800 < *'g:go_jump_to_error'* Use this option to enable/disable passing the bang attribute to the mappings -|(go-build)|, |(go-run)|, etc.. When enabled it will jump to the first error -automatically (means it will NOT pass the bang attribute to the appropriate -command, i.e: (go-run) -> :GoRun ). Note, that calling this doesn't have any -affect on calling the commands manually. This setting is only useful for -changing the behaviour of our custom static mappings. By default it's enabled. +(e.g. |(go-build)|, |(go-run)|, etc.) and the metalinter on save. When +enabled it will jump to the first error automatically (means it will NOT pass +the bang attribute to the appropriate command, i.e: (go-run) -> :GoRun ). +Note, that calling this doesn't have any affect on calling the commands +manually. This setting is only useful for changing the behaviour of our custom +static mappings. By default it's enabled. > let g:go_jump_to_error = 1 < *'g:go_fmt_autosave'* -Use this option to auto |:GoFmt| on save. By default it's enabled > +Use this option to auto |:GoFmt| on save. When both 'g:go_imports_autosave' +and 'g:go_fmt_autosave' are enabled and both 'g:go_fmt_command' and +'g:go_imports_mode' are set to `goimports`, `goimports` will be run only once. +By default it's enabled > let g:go_fmt_autosave = 1 < *'g:go_fmt_command'* -Use this option to define which tool is used to gofmt. By default `gofmt` is -used > +Use this option to define which tool is used to format code. Valid options are +`gofmt`, `goimports`, and `gopls`. By default `gofmt` is used. +> let g:go_fmt_command = "gofmt" < @@ -1315,6 +1382,19 @@ The dictionary version allows you to define options for multiple binaries: \ 'gofmt': '-s', \ 'goimports': '-local mycompany.com', \ } +< + *'b:go_fmt_options'* + +This option is identical to |'g:go_fmt_options'|, but a buffer-level setting. +If present, it's used instead of the global setting. By default it is not set. + +As an example, the following autocmd will configure goimports to put imports +of packages from the current module in their own group: +> + autocmd FileType go let b:go_fmt_options = { + \ 'goimports': '-local ' . + \ trim(system('{cd '. shellescape(expand('%:h')) .' && go list -m;}')), + \ } < *'g:go_fmt_fail_silently'* @@ -1328,10 +1408,31 @@ fails. By default the location list is shown. > Use this option to enable fmt's experimental mode. This experimental mode is superior to the current mode as it fully saves the undo history, so undo/redo doesn't break. However, it's slow (creates/deletes a file for every save) and -it's causing problems on some Vim versions. By default it's disabled. > +it's causing problems on some Vim versions. This has no effect if +`g:go_fmt_command` is set to `gopls`. By default it's disabled. +> let g:go_fmt_experimental = 0 +< + + *'g:go_imports_autosave'* + +Use this option to auto |:GoImports| on save. When both +'g:go_imports_autosave' and 'g:go_fmt_autosave' are enabled and both +'g:go_fmt_command' and 'g:go_imports_mode' are set to `goimports`, `goimports` +will be run only once. By default it's disabled. +> + let g:go_imports_autosave = 0 +< + *'g:go_imports_mode'* + +Use this option to define which tool is used to adjust imports. Valid options +are `goimports` and `gopls`. The buffer will not be formatted when this is set +to `gopls`. By default `goimports` is used. +> + + let g:go_imports_mode = "goimports" < *'g:go_mod_fmt_autosave'* @@ -1344,7 +1445,7 @@ Use this option to auto |:GoModFmt| on save. By default it's enabled > Use this option to run `godoc` on words under the cursor with |K|; this will normally run the `man` program, but for Go using `godoc` is more idiomatic. It -will not override the |'keywordprg'| setting, but will run |:GoDoc|. Default +will not override the 'keywordprg' setting, but will run |:GoDoc|. Default is enabled. > let g:go_doc_keywordprg_enabled = 1 @@ -1359,18 +1460,45 @@ Maximum height for the GoDoc window created with |:GoDoc|. Default is 20. > *'g:go_doc_url'* godoc server URL used when |:GoDocBrowser| is used. Change if you want to use -a private internal service. Default is 'https://godoc.org'. +a private internal service. Default is 'https://pkg.go.dev'. > - let g:go_doc_url = 'https://godoc.org' + let g:go_doc_url = 'https://pkg.go.dev' +< + + *'g:go_doc_popup_window'* + +Use this option to use the popup-window for |K| and |:GoDoc|, rather than the +|preview-window|. Default is disabled. +> + let g:go_doc_popup_window = 0 < *'g:go_def_mode'* Use this option to define the command to be used for |:GoDef|. By default -`guru` is being used as it covers all edge cases. But one might also use -`godef` as it's faster. Current valid options are: `[guru, godef, gopls]` > +`gopls` is used, because it is the fastest. One might also use `guru` for its +accuracy or `godef` for its performance. Valid options are `godef`, `gopls`, +and `guru`. +> + let g:go_def_mode = 'gopls' +< + *'g:go_referrers_mode'* - let g:go_def_mode = 'guru' +Use this option to define the command to be used for |:GoReferrers|. By +default `gopls` is used, because it is the fastest and works with Go modules. +One might also use `guru` for its ability to show references from other +packages. This option will be removed after `gopls` can show references from +other packages. Valid options are `gopls` and `guru`. By default it's `gopls`. +> + let g:go_referrers_mode = 'gopls' +< + *'g:go_implements_mode'* + +Use this option to define the command to be used for |:GoImplements|. +The Implements feature in gopls is still new and being worked upon. +Valid options are `gopls` and `guru`. By default it's `guru`. +> + let g:go_implements_mode = 'guru' < *'g:go_def_mapping_enabled'* @@ -1439,7 +1567,7 @@ pattern. An example input might be: Also see |go-guru-scope|. -By default it's not set, so the relevant commands defaults are being used. +By default it's not set, so the relevant commands' defaults are being used. > let g:go_guru_scope = [] < @@ -1487,7 +1615,7 @@ function when using the `af` text object. By default it's enabled. > Use this option to auto |:GoMetaLinter| on save. Only linter messages for the active buffer will be shown. -By default, `gometalinter` messages will be shown in the |location-list| +By default, `golangci-lint` messages will be shown in the |location-list| window. The list to use can be set using |'g:go_list_type_commands'|. By default it's disabled > @@ -1496,32 +1624,28 @@ window. The list to use can be set using |'g:go_list_type_commands'|. *'g:go_metalinter_autosave_enabled'* Specifies the enabled linters for auto |:GoMetaLinter| on save. By -default it's using `vet` and `golint`. +default it's using `vet` and `golint`. If any are enabled, `--disable-all` +will be sent to the metalinter. > let g:go_metalinter_autosave_enabled = ['vet', 'golint'] < *'g:go_metalinter_enabled'* Specifies the linters to enable for the |:GoMetaLinter| command. By default -it's using `vet`, `golint` and `errcheck`. +it's using `vet`, `golint` and `errcheck`. If any are enabled, `--disable-all` +will be sent to the metalinter. > let g:go_metalinter_enabled = ['vet', 'golint', 'errcheck'] -< - *'g:go_metalinter_disabled'* - -Specifies the linters to disable for the |:GoMetaLinter| command. By default -it's empty -> - let g:go_metalinter_disabled = [] < *'g:go_metalinter_command'* -Overrides the command to be executed when |:GoMetaLinter| is called. By -default it's `gometalinter`. `golangci-lint` is also supported. It can also be -used as an advanced setting for users who want to have more control over -the metalinter. +Overrides the command to be executed when |:GoMetaLinter| is called. By +default it's `golangci-lint`. Valid options are `golangci-lint` and `gopls`. +When the value is `gopls`, users may want to consider setting +`g:go_gopls_staticcheck`. It can also be used as an advanced setting for +users who want to have more control over the metalinter. > - let g:go_metalinter_command = "gometalinter" + let g:go_metalinter_command = "golangci-lint" < *'g:go_metalinter_deadline'* @@ -1586,19 +1710,29 @@ Use this option to auto |:AsmFmt| on save. By default it's disabled. > < *'g:go_term_mode'* -This option is Neovim only. Use it to change the default command used to -open a new terminal for go commands such as |:GoRun|. -The default is vsplit. +The default command used to open a new terminal for go commands such as +|:GoRun|. The default is `:vsplit`. + +Applicable to Neovim and Vim with `terminal` feature only. > let g:go_term_mode = "vsplit" +< + *'g:go_term_reuse'* + +Reuse the terminal window when |'g:go_term_enabled'| is set. By default it's +disabled. +> + let g:go_term_reuse = 0 < *'g:go_term_height'* *'g:go_term_width'* -These options are Neovim only. Use them to control the height and width of -a terminal split. By default these are not set, meaning that the height and -width are set automatically by Neovim. The height only applies to a -horizontal split and width only applies to a vertical split. +Controls the height and width of a terminal split, respectively. By default +these are not set, meaning that the height and width are set automatically by +the editor. The height only applies to a horizontal split and width only +applies to a vertical split. + +Applicable to Neovim and Vim with `terminal` feature only. For example here is how to set each to 30. > @@ -1607,19 +1741,36 @@ For example here is how to set each to 30. < *'g:go_term_enabled'* -This option is Neovim only. Use it to change the behavior of the test -commands. If set to 1 it opens the test commands inside a new terminal -according to |'g:go_term_mode'|, otherwise it will run them in the background -just like `:GoBuild`. By default it is disabled. +Causes some types of jobs to run inside a new terminal according to +|'g:go_term_mode'|. By default it is disabled. + +Applicable to Neovim and Vim with `terminal` feature only. > let g:go_term_enabled = 0 +< + *'g:go_term_close_on_exit'* + +Closes the terminal after the command run in it exits when the command fails. +By default it is enabled. + +Applicable to Neovim and Vim with `terminal` feature only. + +> + let g:go_term_close_on_exit = 1 < *'g:go_alternate_mode'* -Specifies the command that |:GoAlternate| uses to open the alternate file. -By default it is set to edit. +Specifies the command that |:GoAlternate| uses to open the alternate file. By +default it is set to edit. > let g:go_alternate_mode = "edit" +< + *'g:go_rename_command'* + +Use this option to define which tool is used to rename. By default `gopls` +is used. Valid options are `gorename` and `gopls`. +> + let g:go_rename_command = 'gopls' < *'g:go_gorename_prefill'* @@ -1633,42 +1784,130 @@ same. \ '? go#util#pascalcase(expand(""))' . \ ': go#util#camelcase(expand(""))' < - *'g:go_gocode_propose_builtins'* -Specifies whether `gocode` should add built-in types, functions and constants -to an autocompletion proposals. By default it is enabled. -> - let g:go_gocode_propose_builtins = 1 -< - *'g:go_gocode_propose_source'* + *'g:go_gopls_enabled'* -Specifies whether `gocode` should use source files instead of binary packages -for autocompletion proposals. When disabled, only identifiers from the current -package and packages that have been installed will proposed. -> - let g:go_gocode_propose_source = 0 -< - *'g:go_gocode_unimported_packages'* +Specifies whether `gopls` can be used by vim-go. By default gopls is enabled. +When gopls is disabled completion will not work and other configuration +options may also need to be adjusted. -Specifies whether `gocode` should include suggestions from unimported -packages. By default it is disabled. > - let g:go_gocode_unimported_packages = 0 + let g:go_gopls_enabled = 1 < - *'g:go_gocode_socket_type'* + *'g:go_gopls_options'* -Specifies whether `gocode` should use a different socket type. By default -`unix` is enabled. Possible values: `unix`, `tcp` +The commandline arguments to pass to gopls. By default, it's `-remote=auto`. > - let g:go_gocode_socket_type = 'unix' + let g:go_gopls_options = [] < + + *'g:go_gopls_analyses'* + +The analyses settings for `gopls`. By default, it's `v:null`. Valid map values +are `v:true` and `v:false`. +> + let g:go_gopls_analyses = v:null +< + + *'g:go_gopls_complete_unimported'* + +Specifies whether `gopls` should include suggestions from unimported packages. +When it is `v:null`, `gopls`' default will be used. By default it is +`v:null`. +> + let g:go_gopls_complete_unimported = v:null +< + + *'g:go_gopls_deep_completion'* + +Specifies whether `gopls` should use deep completion. When it is `v:null`, +`gopls`' default will be used. By default it is `v:null`. + +> + let g:go_gopls_deep_completion = v:null +< + + *'g:go_gopls_matcher'* + +Specifies how `gopls` should match for completions. Valid values are `v:null`, +`fuzzy`, and `caseSensitive`. When it is `v:null`, `gopls`' default will be +used. By default it is `v:null`. +> + let g:go_gopls_matcher = v:null +< + + *'g:go_gopls_staticcheck'* + +Specifies whether `gopls` should run staticcheck checks. When it is `v:null`, +`gopls`' default will be used. By default it is `v:null`. +> + let g:go_gopls_staticcheck = v:null +< + + *'g:go_gopls_use_placeholders'* + +Specifies whether `gopls` can provide placeholders for function parameters and +struct fields. When set, completion items will be treated as anonymous +snippets if UltiSnips is installed and configured to be used as +|'g:go_snippet_engine'|. When it is `v:null`, `gopls`' default will be used. +By default it is `v:null`. +> + let g:go_gopls_use_placeholders = v:null +< + + *'g:go_gopls_temp_modfile'* + +Specifies whether `gopls` should use a temp modfile and suggest edits rather +than modifying the ambient go.mod file. When it is `v:null`, `gopls`' default +will be used. By default it is `v:null`. +> + let g:go_gopls_temp_modfile = v:null +< + + *'g:go_gopls_local'* + +Specifies the prefix for imports that `gopls` should consider group +separately. When it is `v:null`, `gopls`' default will be used. By default it +is `v:null`. +> + let g:go_gopls_local = v:null +< + + *'g:go_gopls_gofumpt'* + +Specifies whether `gopls` should use `gofumpt` for formatting. When it is +`v:null`, `gopls`' default will be used. By default it is `v:null`. +> + let g:go_gopls_gofumpt = v:null +< + + *'g:go_gopls_settings'* + +Specifies `gopls` workspace settings for `gopls` that are not yet officially +supported by vim-go. Any value in the dictionary will be overridden by values +provided in the specific options supported by vim-go (e.g. +g:go_gopls_staticcheck) or settings statically configured by vim-go to ensure +expected behavior. By default it is `v:null`. +> + let g:go_gopls_settings = v:null +< + *'g:go_diagnostics_enabled'* + +Specifies whether `gopls` diagnostics are enabled. Only the diagnostics for +the current buffer will be processed when it is not set; all others will be +ignored. By default it is disabled. +> + let g:go_diagnostics_enabled = 0 +< + *'g:go_template_autocreate'* When a new Go file is created, vim-go automatically fills the buffer content with a Go code template. By default, the templates under the `templates` folder are used. This can be changed with the |'g:go_template_file'| and -|'g:go_template_test_file'| settings. +|'g:go_template_test_file'| settings to either use a different file in the +same `templates` folder, or to use a file stored elsewhere. If the new file is created in an already prepopulated package (with other Go files), in this case a Go code template with only the Go package declaration @@ -1683,17 +1922,23 @@ By default it is enabled. < *'g:go_template_file'* -Specifies the file under the `templates` folder that is used if a new Go file -is created. Checkout |'g:go_template_autocreate'| for more info. By default -the `hello_world.go` file is used. +Specifies either the file under the `templates` folder that is used if a new +Go file is created. Checkout |'g:go_template_autocreate'| for more info. By +default the `hello_world.go` file is used. + +This variable can be set to an absolute path, so the template files don't have +to be stored inside the vim-go directory structure. Useful when you want to +use different templates for different projects. > let g:go_template_file = "hello_world.go" < *'g:go_template_test_file'* -Specifies the file under the `templates` folder that is used if a new Go test -file is created. Checkout |'g:go_template_autocreate'| for more info. By -default the `hello_world_test.go` file is used. +Like with |'g:go_template_file'|, this specifies the file to use for test +tempaltes. The template file should be under the `templates` folder, +alternatively absolute paths can be used, too. Checkout +|'g:go_template_autocreate'| for more info. By default, the +`hello_world_test.go` file is used. > let g:go_template_test_file = "hello_world_test.go" < @@ -1733,8 +1978,8 @@ i.e: |go#statusline#Show()|. By default it's enabled < *'g:go_echo_go_info'* -Use this option to show the identifier information when completion is done. By -default it's enabled > +Use this option to show the identifier information when code completion is +done. By default it's enabled. > let g:go_echo_go_info = 1 < @@ -1751,24 +1996,38 @@ default it's 60 seconds. Must be in milliseconds. Sets the `transform` option for `gomodifytags` when using |:GoAddTags| or if it's being used for snippet expansion of single fields. Possible options are: -`snakecase`, `camelcase`. For the following case, if `snakecase` is used the -field will be transformed to: +`snakecase`, `camelcase`, `lispcase`, `pascalcase`, `keep`. For the following +case, if `snakecase` is used the field will be transformed to: > type T struct { - FooBarQuz string `json:"foo_bar_quz" + FooBarQuz string `json:"foo_bar_quz"` } < If "camelcase" is used: > type T struct { - FooBarQuz string `json:"fooBarQuz" + FooBarQuz string `json:"fooBarQuz"` } < By default "snakecase" is used. Current values are: ["snakecase", -"camelcase"]. +"camelcase", "lispcase", "pascalcase", "keep"]. > let g:go_addtags_transform = 'snakecase' +< + *'g:go_addtags_skip_unexported'* + +Sets the `skip-unexported` option for `gomodifytags` when using |:GoAddTags|. +If set it will prevent `gomodifytags` from adding tags to unexported fields: +> + type T struct { + FooBar string `json:"foo_bar"` + quz string + } +< +By default it is disabled. +> + let g:go_addtags_skip_unexported = 0 < *'g:go_debug'* @@ -1780,7 +2039,10 @@ Currently accepted values: debugger-state Expose debugger state in 'g:go_debug_diag'. debugger-commands Echo communication between vim-go and `dlv`; requests and responses are recorded in `g:go_debug_commands`. - lsp Record lsp requests and responses in g:go_lsp_log. + lsp Echo communication between vim-go and `gopls`. All + communication is shown in a dedicated window. When + enabled before gopls is started, |:GoLSPDebugBrowser| can + be used to open a browser window to help debug gopls. > let g:go_debug = [] < @@ -1924,6 +2186,18 @@ Highlight variable names in variable assignments (`x` in `x =`). > let g:go_highlight_variable_assignments = 0 < + *'g:go_highlight_diagnostic_errors'* + +Highlight diagnostic errors. +> + let g:go_highlight_diagnostic_errors = 1 +< + *'g:go_highlight_diagnostic_warnings'* + +Highlight diagnostic warnings. +> + let g:go_highlight_diagnostic_warnings = 1 +< ============================================================================== *gohtmltmpl* *ft-gohtmltmpl-syntax* @@ -1991,6 +2265,8 @@ The program will halt on the breakpoint, at which point you can inspect the program state. You can go to the next line with |:GoDebugNext| () or step in with |:GoDebugStep| (). +The program can also be halted with `:GoDebugHalt` (). + The variable window in the bottom left (`GODEBUG_VARIABLES`) will display all local variables. Struct values are displayed as `{...}`, array/slices as `[4]`. Use on the variable name to expand the values. @@ -2007,8 +2283,21 @@ the `dlv` process, or |:GoDebugRestart| to recompile the code. *go-debug-commands* DEBUGGER COMMANDS~ -Only |:GoDebugStart| and |:GoDebugBreakpoint| are available by default; the -rest of the commands and mappings become available after starting debug mode. +Only |:GoDebugAttach|, |:GoDebugStart|, |:GoDebugTest|, and +|:GoDebugBreakpoint| are available by default. |:GoDebugContinue| becomes +available after running |:GoDebugAttach|, |:GoDebugStart| or |:GoDebugTest|. +The rest of the commands and mappings become available after executing +|:GoDebugContinue|. + + *:GoDebugAttach* +:GoDebugAttach pid + + Start the debug mode for pid; this does several things: + + * Setup the debug windows according to |'g:go_debug_windows'|. + * Make the `:GoDebug*` commands and `(go-debug-*)` mappings available. + + Use |:GoDebugStop| to stop `dlv` and exit debugging mode. *:GoDebugStart* :GoDebugStart [pkg] [program-args] @@ -2018,8 +2307,8 @@ rest of the commands and mappings become available after starting debug mode. * Setup the debug windows according to |'g:go_debug_windows'|. * Make the `:GoDebug*` commands and `(go-debug-*)` mappings available. - The current directory is used if [pkg] is empty. Any other arguments will - be passed to the program. + The directory of the current buffer is used if [pkg] is empty. Any other + arguments will be passed to the program. Use |:GoDebugStop| to stop `dlv` and exit debugging mode. @@ -2032,13 +2321,20 @@ rest of the commands and mappings become available after starting debug mode. Use `-test.flag` to pass flags to `go test` when debugging a test; for example `-test.v` or `-test.run TestFoo` - *:GoDebugRestart* :GoDebugRestart Stop the program (if running) and restart `dlv` to recompile the package. The current window layout and breakpoints will be left intact. + *:GoDebugHalt* + *(go-debug-halt)* +:GoDebugHalt + + Halt the program. + + Mapped to by default. + *:GoDebugStop* *(go-debug-stop)* :GoDebugStop @@ -2076,7 +2372,6 @@ rest of the commands and mappings become available after starting debug mode. Advance execution by one line, also called "step over" by some other debuggers. - It will behave as |:GoDebugContinue| if the program isn't started. Mapped to by default. @@ -2086,7 +2381,6 @@ rest of the commands and mappings become available after starting debug mode. Advance execution by one step, stopping at the next line of code that will be executed (regardless of location). - It will behave as |:GoDebugContinue| if the program isn't started. Mapped to by default. @@ -2097,7 +2391,6 @@ rest of the commands and mappings become available after starting debug mode. Run all the code in the current function and halt when the function returns ("step out of the current function"). - It will behave as |:GoDebugContinue| if the program isn't started. *:GoDebugSet* :GoDebugSet {var} {value} @@ -2127,17 +2420,20 @@ DEBUGGER SETTINGS~ *'g:go_debug_windows'* -Controls the window layout for debugging mode. This is a |dict| with three -possible keys: "stack", "out", and "vars"; the windows will created in that -order with the commands in the value. +Controls the window layout for debugging mode. This is a |dict| with four +possible keys: "vars", "stack", "goroutines", and "out"; each of the new +windows will be created in that that order with the commands in the value. The +current window is made the only window before creating the debug windows. + A window will not be created if a key is missing or empty. Defaults: > let g:go_debug_windows = { - \ 'stack': 'leftabove 20vnew', - \ 'out': 'botright 10new', - \ 'vars': 'leftabove 30vnew', + \ 'vars': 'leftabove 30vnew', + \ 'stack': 'leftabove 20new', + \ 'goroutines': 'botright 10new', + \ 'out': 'botright 5new', \ } < Show only variables on the right-hand side: > @@ -2156,12 +2452,11 @@ Defaults to `127.0.0.1:8181`: *'g:go_debug_log_output'* -Specifies log output options for `dlv`. Value should be a single string -of comma-separated options suitable for passing to `dlv`. An empty string -(`''`) will suppress logging entirely. -Default: `'debugger, rpc'`: +Specifies log output options for `dlv`. Value should be a single string of +comma-separated options suitable for passing to `dlv`. An empty string (`''`) +will suppress logging entirely. Default: `'debugger,rpc'`: > - let g:go_debug_log = 'debugger, rpc' + let g:go_debug_log_output = 'debugger,rpc' < *'g:go_highlight_debug'* @@ -2172,9 +2467,63 @@ Highlight the current line and breakpoints in the debugger. let g:go_highlight_debug = 1 < + *'go:go_debug_breakpoint_sign_text'* + +Set the sign text used for breakpoints in the debugger. By default it's '>'. + +> + let g:go_debug_breakpoint_sign_text = '>' +< + ============================================================================== FAQ TROUBLESHOOTING *go-troubleshooting* +How do I troubleshoot problems?~ + +One of the best ways to understand what vim-go is doing and the output from +the tools to which it delegates is to use leverage the features described in +|'g:go_debug'|. + +Completion and other functions that use `gopls` don't work~ + +Vim-go is heavily reliant on `gopls` for completion and other functionality. +Many of the features that use `gopls` (e.g. completion, jumping to +definitions, showing identifier information, et al.) can be configured to +delegate to other tools. e.g. completion via 'omnifunc', |'g:go_info_mode'| +and |'g:go_def_mode'| can be set to use other tools for now (though some of +the alternatives to `gopls` are effectively at their end of life and support +for them from within vim-go may be removed soon). + +I want to disable `gopls`~ + +Vim-go's use of `gopls` can be disabled with 'g:go_gopls_enabled'. + +Some users want to do this to limit the load on their system when using vim-go +concurrently with an LSP client like vim-lsp. Instead of disabling vim-go's +use of `gopls`, you may prefer to configure vim-go to share the `gopls` +instance with other LSP plugins. 'g:go_gopls_options' can be used to configure +how vim-go starts `gopls` so that the instance can be shared with other +plugins and vim-go user's can leverage the full power of vim-go. + +I get a "Unknown function: go#config#..." error~ + +This often happens to vim-polyglot users when new config options are added to +vim-go. Run vim-polyglot's `build` script or make sure that vim-go is loaded +before vim-polyglot. + +It can also happen when multiple versions of vim-go are installed and the +version loaded by Vim doesn't have a function introduced by a later version. +To see where vim-go is being loaded from run +> + :verbose function go#config#FmtAutosave +< + +The output will show the path to the `autoload/go/config.vim` that was loaded +by Vim. Make sure the root of the path to output by the command is the path +from which vim-go is expected to sourced. If it is not rooted as expected, +then there are multiple copies of vim-go installed; remove the unexpected +copies. + I get "not an editor command" error when I invoke :GoXXX~ This happens if vim-go is not installed properly. Be sure you have added this @@ -2289,9 +2638,9 @@ while saving and opening files. The following fixes this: let g:syntastic_go_checkers = ['golint', 'govet'] let g:syntastic_mode_map = { 'mode': 'active', 'passive_filetypes': ['go'] } < -If you want to add errcheck you can use gometalinter as a wrapper +If you want to add errcheck you can use golangci-lint as a wrapper: > - let g:syntastic_go_checkers = ['golint', 'govet', 'gometalinter'] + let g:syntastic_go_checkers = ['golint', 'govet', 'golangci-lint'] let g:syntastic_go_gometalinter_args = ['--disable-all', '--enable=errcheck'] let g:syntastic_mode_map = { 'mode': 'active', 'passive_filetypes': ['go'] } < @@ -2365,7 +2714,7 @@ To run tests vim-go comes with three small helper scripts: `scripts/test` Run all tests with a Vim from `/tmp/vim-go-test/`. All scripts accept a Vim version as the first argument, which can be -`vim-7.4`, `vim-8.0`, or `nvim`. You will need to install a Vim version with +`vim-8.0` or `nvim`. You will need to install a Vim version with `install-vim` before you can use `run-vim` or `test`. You can install and test all Vim versions by running `make`. diff --git a/pack/acp/start/vim-go/ftplugin/go.vim b/pack/acp/start/vim-go/ftplugin/go.vim index 233a47a..687f881 100644 --- a/pack/acp/start/vim-go/ftplugin/go.vim +++ b/pack/acp/start/vim-go/ftplugin/go.vim @@ -13,8 +13,8 @@ let b:did_ftplugin = 1 let s:cpo_save = &cpo set cpo&vim -let b:undo_ftplugin = "setl fo< com< cms< - \ | exe 'au! vim-go-buffer * '" +let b:undo_ftplugin = "setl fo< com< cms<" + \ . "| exe 'au! vim-go-buffer * '" setlocal formatoptions-=t @@ -25,10 +25,9 @@ setlocal noexpandtab compiler go -" Set autocompletion -setlocal omnifunc=go#complete#Complete -if !go#util#has_job() - setlocal omnifunc=go#complete#GocodeComplete +if go#config#CodeCompletionEnabled() + " Set autocompletion + setlocal omnifunc=go#complete#Complete endif if get(g:, "go_doc_keywordprg_enabled", 1) @@ -72,43 +71,59 @@ if get(g:, "go_textobj_enabled", 1) xnoremap [[ :call go#textobj#FunctionJump('v', 'prev') endif -if go#config#AutoTypeInfo() || go#config#AutoSameids() - let &l:updatetime= get(g:, "go_updatetime", 800) -endif - " Autocommands " ============================================================================ " augroup vim-go-buffer autocmd! * - " The file is registered (textDocument/DidOpen) with gopls in in - " plugin/go.vim on the FileType event. - " TODO(bc): handle all the other events that may be of interest to gopls, - " too (e.g. BufFilePost , CursorHold , CursorHoldI, FileReadPost, - " StdinReadPre, BufWritePost, TextChange, TextChangedI) + " The file is registered (textDocument/DidOpen) with gopls in plugin/go.vim + " on the FileType event. + if go#util#has_job() - autocmd BufWritePost call go#lsp#DidChange(expand(':p')) - autocmd FileChangedShell call go#lsp#DidChange(expand(':p')) + autocmd BufWritePost,FileChangedShellPost call go#lsp#DidChange(expand(':p')) autocmd BufDelete call go#lsp#DidClose(expand(':p')) endif - autocmd CursorHold call go#auto#auto_type_info() - autocmd CursorHold call go#auto#auto_sameids() + " send the textDocument/didChange notification when idle. go#lsp#DidChange + " will not send an event if the buffer hasn't changed since the last + " notification. + autocmd CursorHold,CursorHoldI call go#lsp#DidChange(expand(':p')) + + autocmd BufEnter,CursorHold call go#auto#update_autocmd() " Echo the identifier information when completion is done. Useful to see " the signature of a function, etc... if exists('##CompleteDone') - autocmd CompleteDone call go#auto#echo_go_info() + autocmd CompleteDone call go#auto#complete_done() endif autocmd BufWritePre call go#auto#fmt_autosave() autocmd BufWritePost call go#auto#metalinter_autosave() - " clear SameIds when the buffer is unloaded so that loading another buffer - " in the same window doesn't highlight the most recently matched - " identifier's positions. - autocmd BufWinEnter call go#guru#ClearSameIds() + if !has('textprop') + "TODO(bc): how to clear sameids and diagnostics when a non-go buffer is + " loaded into a window and the previously loaded buffer is still loaded in + " another window? + + " TODO(bc): only clear when the new buffer isn't the old buffer + + " clear SameIds when the buffer is unloaded from its last window so that + " loading another buffer (especially of a different filetype) in the same + " window doesn't highlight the most recently matched identifier's positions. + autocmd BufWinLeave call go#guru#ClearSameIds() + " clear SameIds when a new buffer is loaded in the window so that the + " previous buffer's highlighting isn't used. + autocmd BufWinEnter call go#guru#ClearSameIds() + + " clear diagnostics when the buffer is unloaded from its last window so that + " loading another buffer (especially of a different filetype) in the same + " window doesn't highlight the previously loaded buffer's diagnostics. + autocmd BufWinLeave call go#lsp#ClearDiagnosticHighlights() + " clear diagnostics when a new buffer is loaded in the window so that the + " previous buffer's diagnostics aren't used. + "autocmd BufWinEnter call go#lsp#ClearDiagnosticHighlights() + endif autocmd BufEnter \ if go#config#AutodetectGopath() && !exists('b:old_gopath') diff --git a/pack/acp/start/vim-go/ftplugin/go/commands.vim b/pack/acp/start/vim-go/ftplugin/go/commands.vim index 726b944..ebc911b 100644 --- a/pack/acp/start/vim-go/ftplugin/go/commands.vim +++ b/pack/acp/start/vim-go/ftplugin/go/commands.vim @@ -3,7 +3,7 @@ command! -nargs=? -complete=customlist,go#rename#Complete GoRename call go#renam " -- guru command! -nargs=* -complete=customlist,go#package#Complete GoGuruScope call go#guru#Scope() -command! -range=% GoImplements call go#guru#Implements() +command! -range=% GoImplements call go#implements#Implements() command! -range=% GoPointsTo call go#guru#PointsTo() command! -range=% GoWhicherrs call go#guru#Whicherrs() command! -range=% GoCallees call go#guru#Callees() @@ -12,7 +12,7 @@ command! -range=% GoCallers call go#guru#Callers() command! -range=% GoCallstack call go#guru#Callstack() command! -range=% GoFreevars call go#guru#Freevars() command! -range=% GoChannelPeers call go#guru#ChannelPeers() -command! -range=% GoReferrers call go#guru#Referrers() +command! -range=% GoReferrers call go#referrers#Referrers() command! -range=0 GoSameIds call go#guru#SameIds(1) command! -range=0 GoSameIdsClear call go#guru#ClearSameIds() @@ -105,8 +105,9 @@ command! -nargs=0 GoFillStruct call go#fillstruct#FillStruct() " -- debug if !exists(':GoDebugStart') - command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start(0, ) - command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start(1, ) + command! -nargs=* -complete=customlist,go#package#Complete GoDebugStart call go#debug#Start('debug', ) + command! -nargs=* -complete=customlist,go#package#Complete GoDebugTest call go#debug#Start('test', ) + command! -nargs=1 GoDebugAttach call go#debug#Start('attach', ) command! -nargs=? GoDebugBreakpoint call go#debug#Breakpoint() endif @@ -116,4 +117,12 @@ command! -nargs=0 GoReportGitHubIssue call go#issue#New() " -- iferr command! -nargs=0 GoIfErr call go#iferr#Generate() +" -- lsp +command! -nargs=+ -complete=dir GoAddWorkspace call go#lsp#AddWorkspaceDirectory() +command! -nargs=0 GoLSPDebugBrowser call go#lsp#DebugBrowser() +command! -nargs=* -bang GoDiagnostics call go#lint#Diagnostics(0, ) + +" -- term +command! GoToggleTermCloseOnExit call go#term#ToggleCloseOnExit() + " vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/ftplugin/go/mappings.vim b/pack/acp/start/vim-go/ftplugin/go/mappings.vim index f1f069e..16e4db6 100644 --- a/pack/acp/start/vim-go/ftplugin/go/mappings.vim +++ b/pack/acp/start/vim-go/ftplugin/go/mappings.vim @@ -11,7 +11,7 @@ endif " Some handy plug mappings nnoremap (go-run) :call go#cmd#Run(!g:go_jump_to_error) -if has("nvim") +if has("nvim") || has("terminal") nnoremap (go-run-vertical) :call go#cmd#RunTerm(!g:go_jump_to_error, 'vsplit', []) nnoremap (go-run-split) :call go#cmd#RunTerm(!g:go_jump_to_error, 'split', []) nnoremap (go-run-tab) :call go#cmd#RunTerm(!g:go_jump_to_error, 'tabe', []) @@ -35,14 +35,14 @@ nnoremap (go-info) :call go#tool#Info(1) nnoremap (go-import) :call go#import#SwitchImport(1, '', expand(''), '') nnoremap (go-imports) :call go#fmt#Format(1) -nnoremap (go-implements) :call go#guru#Implements(-1) +nnoremap (go-implements) :call go#implements#Implements(-1) nnoremap (go-callees) :call go#guru#Callees(-1) nnoremap (go-callers) :call go#guru#Callers(-1) nnoremap (go-describe) :call go#guru#Describe(-1) nnoremap (go-callstack) :call go#guru#Callstack(-1) xnoremap (go-freevars) :call go#guru#Freevars(0) nnoremap (go-channelpeers) :call go#guru#ChannelPeers(-1) -nnoremap (go-referrers) :call go#guru#Referrers(-1) +nnoremap (go-referrers) :call go#referrers#Referrers(-1) nnoremap (go-sameids) :call go#guru#SameIds(1) nnoremap (go-pointsto) :call go#guru#PointsTo(-1) nnoremap (go-whicherrs) :call go#guru#Whicherrs(-1) @@ -83,4 +83,6 @@ nnoremap (go-alternate-split) :call go#alternate#Switch(0, " nnoremap (go-iferr) :call go#iferr#Generate() +nnoremap (go-diagnostics) :call go#lint#Diagnostics(!g:go_jump_to_error) + " vim: sw=2 ts=2 et diff --git a/pack/acp/start/vim-go/ftplugin/go/snippets.vim b/pack/acp/start/vim-go/ftplugin/go/snippets.vim index 1fbc1ab..00bcbd4 100644 --- a/pack/acp/start/vim-go/ftplugin/go/snippets.vim +++ b/pack/acp/start/vim-go/ftplugin/go/snippets.vim @@ -52,19 +52,11 @@ endfunction let s:engine = go#config#SnippetEngine() -if s:engine is? "automatic" - if get(g:, 'did_plugin_ultisnips') is 1 - call s:GoUltiSnips() - elseif get(g:, 'loaded_neosnippet') is 1 - call s:GoNeosnippet() - elseif get(g:, 'loaded_minisnip') is 1 - call s:GoMinisnip() - endif -elseif s:engine is? "ultisnips" +if s:engine is? 'ultisnips' call s:GoUltiSnips() -elseif s:engine is? "neosnippet" +elseif s:engine is? 'neosnippet' call s:GoNeosnippet() -elseif s:engine is? "minisnip" +elseif s:engine is? 'minisnip' call s:GoMinisnip() endif diff --git a/pack/acp/start/vim-go/gosnippets/UltiSnips/go.snippets b/pack/acp/start/vim-go/gosnippets/UltiSnips/go.snippets index 0a9b7ae..ba39cd2 100644 --- a/pack/acp/start/vim-go/gosnippets/UltiSnips/go.snippets +++ b/pack/acp/start/vim-go/gosnippets/UltiSnips/go.snippets @@ -144,20 +144,42 @@ else { endsnippet # if inline error -snippet ife "If with inline erro" +snippet ife "If with inline error" if err := ${1:condition}; err != nil { ${0:${VISUAL}} } endsnippet +snippet ew "errors.Wrap" +errors.Wrap(${1:err}, "${2:message}") +endsnippet + +snippet ewf "errors.Wrapf" +errors.Wrapf(${1:err}, "${2:message %v}", ${3:args...}) +endsnippet + # error snippet -snippet errn "Error return " !b +snippet errn "Error return" !b if err != nil { return err } ${0} endsnippet +snippet errnw "Error return wrap" !b +if err != nil { + return errors.Wrap(err, "${1:message}") +} +${0} +endsnippet + +snippet errnwf "Error return wrapf" !b +if err != nil { + return errors.Wrapf(err, "${1:message %v}", ${2:args...}) +} +${0} +endsnippet + # error log snippet snippet errl "Error with log.Fatal(err)" !b if err != nil { @@ -174,6 +196,20 @@ if err != nil { ${0} endsnippet +snippet errn,w "Error return wrap with two return values" !b +if err != nil { + return nil, errors.Wrap(err, "${1:message}") +} +${0} +endsnippet + +snippet errn,wf "Error return wrapf with two return values" !b +if err != nil { + return nil, errors.Wrapf(err, "${1:message %v}", ${2:args...}) +} +${0} +endsnippet + # error panic snippet errp "Error panic" !b if err != nil { @@ -235,6 +271,20 @@ for ${2:k}, ${3:v} := range ${1} { } endsnippet +snippet forsel "for select" +for { + select { + case ${2:${1:result} := }<- ${3:channel}: + ${0} + } +} +endsnippet + +snippet selc "select case" !b +case ${1:${2:var} := }<-${3:channel}: + ${0} +endsnippet + # function snippet func "func Function(...) [error] { ... }" func ${1:name}(${2:params})${3/(.+)/ /}`!p opening_par(snip, 3)`$3`!p closing_par(snip, 3)` { @@ -244,7 +294,12 @@ endsnippet # Fmt Printf debug snippet ff "fmt.Printf(...)" -fmt.Printf("${1:${VISUAL}} = %+v\n", $1) +fmt.Printf("$1 = %+v\n", ${1:${VISUAL}}) +endsnippet + +# Fmt Printf debug with hash +snippet ffh "fmt.Printf(#...) hash" +fmt.Printf("$1 = %#v\n", ${1:${VISUAL}}) endsnippet # Fmt Println debug @@ -257,6 +312,18 @@ snippet fe "fmt.Errorf(...)" fmt.Errorf("${1:${VISUAL}}") endsnippet +# Fmt Errorf wrap +snippet few "fmt.Errorf(%w, err)" +fmt.Errorf("${1:message}: %w", ${2:${VISUAL:err}}) +endsnippet + +# Fmt Errorf wrap and return +snippet errnfw "Error return fmt.Errorf(%w, err)" !b +if ${1:${VISUAL:err}} != nil { + return fmt.Errorf("${2:message}: %w", $1) +} +endsnippet + # log printf snippet lf "log.Printf(...)" log.Printf("${1:${VISUAL}} = %+v\n", $1) @@ -326,7 +393,7 @@ endsnippet # struct snippet st "type T struct { ... }" type ${1:Type} struct { -${0} + ${0} } endsnippet @@ -338,6 +405,12 @@ case ${2:value1}: } endsnippet +snippet tswitch "type switch x { ... }" +switch ${2:$1 := }${1:v}.(type) { + ${0} +} +endsnippet + # sprintf snippet sp "fmt.Sprintf(...)" fmt.Sprintf("%${1:s}", ${2:var}) @@ -385,7 +458,7 @@ for _, tt := range tests { endsnippet -snippet hf "http.HandlerFunc" !b +snippet hf "http.HandlerFunc" func ${1:handler}(w http.ResponseWriter, r *http.Request) { ${0:fmt.Fprintf(w, "hello world")} } diff --git a/pack/acp/start/vim-go/plugin/go.vim b/pack/acp/start/vim-go/plugin/go.vim index c68526e..79893fc 100644 --- a/pack/acp/start/vim-go/plugin/go.vim +++ b/pack/acp/start/vim-go/plugin/go.vim @@ -9,30 +9,22 @@ let s:cpo_save = &cpo set cpo&vim function! s:checkVersion() abort - " Not using the has('patch-7.4.2009') syntax because that wasn't added until - " 7.4.237, and we want to be sure this works for everyone (this is also why - " we're not using utils#EchoError()). - " - " Version 7.4.2009 was chosen because that's greater than what the most recent Ubuntu LTS - " release (16.04) uses and has a couple of features we need (e.g. execute() - " and :message clear). - let l:unsupported = 0 if go#config#VersionWarning() != 0 if has('nvim') - let l:unsupported = !has('nvim-0.3.1') + let l:unsupported = !has('nvim-0.4.0') else - let l:unsupported = (v:version < 704 || (v:version == 704 && !has('patch2009'))) + let l:unsupported = !has('patch-8.0.1453') endif if l:unsupported == 1 echohl Error - echom "vim-go requires Vim 7.4.2009 or Neovim 0.3.1, but you're using an older version." + echom "vim-go requires at least Vim 8.0.1453 or Neovim 0.4.0, but you're using an older version." echom "Please update your Vim for the best vim-go experience." echom "If you really want to continue you can set this to make the error go away:" echom " let g:go_version_warning = 0" echom "Note that some features may error out or behave incorrectly." - echom "Please do not report bugs unless you're using Vim 7.4.2009 or newer or Neovim 0.3.1." + echom "Please do not report bugs unless you're using at least Vim 8.0.1453 or Neovim 0.4.0." echohl None " Make sure people see this. @@ -45,28 +37,26 @@ call s:checkVersion() " these packages are used by vim-go and can be automatically installed if " needed by the user with GoInstallBinaries. + +" NOTE(bc): varying the binary name and the tail of the import path does not yet work in module aware mode. let s:packages = { - \ 'asmfmt': ['github.com/klauspost/asmfmt/cmd/asmfmt'], - \ 'dlv': ['github.com/go-delve/delve/cmd/dlv'], - \ 'errcheck': ['github.com/kisielk/errcheck'], - \ 'fillstruct': ['github.com/davidrjenni/reftools/cmd/fillstruct'], - \ 'gocode': ['github.com/mdempsky/gocode', {'windows': ['-ldflags', '-H=windowsgui']}], - \ 'gocode-gomod': ['github.com/stamblerre/gocode'], - \ 'godef': ['github.com/rogpeppe/godef'], - \ 'gogetdoc': ['github.com/zmb3/gogetdoc'], - \ 'goimports': ['golang.org/x/tools/cmd/goimports'], - \ 'golint': ['golang.org/x/lint/golint'], - \ 'gopls': ['golang.org/x/tools/cmd/gopls'], - \ 'gometalinter': ['github.com/alecthomas/gometalinter'], - \ 'golangci-lint': ['github.com/golangci/golangci-lint/cmd/golangci-lint'], - \ 'gomodifytags': ['github.com/fatih/gomodifytags'], - \ 'gorename': ['golang.org/x/tools/cmd/gorename'], - \ 'gotags': ['github.com/jstemmer/gotags'], - \ 'guru': ['golang.org/x/tools/cmd/guru'], - \ 'impl': ['github.com/josharian/impl'], - \ 'keyify': ['honnef.co/go/tools/cmd/keyify'], - \ 'motion': ['github.com/fatih/motion'], - \ 'iferr': ['github.com/koron/iferr'], + \ 'asmfmt': ['github.com/klauspost/asmfmt/cmd/asmfmt@master'], + \ 'dlv': ['github.com/go-delve/delve/cmd/dlv@master'], + \ 'errcheck': ['github.com/kisielk/errcheck@master'], + \ 'fillstruct': ['github.com/davidrjenni/reftools/cmd/fillstruct@master'], + \ 'godef': ['github.com/rogpeppe/godef@master'], + \ 'goimports': ['golang.org/x/tools/cmd/goimports@master'], + \ 'golint': ['golang.org/x/lint/golint@master'], + \ 'gopls': ['golang.org/x/tools/gopls@latest', {}, {'after': function('go#lsp#Restart', [])}], + \ 'golangci-lint': ['github.com/golangci/golangci-lint/cmd/golangci-lint@master'], + \ 'gomodifytags': ['github.com/fatih/gomodifytags@master'], + \ 'gorename': ['golang.org/x/tools/cmd/gorename@master'], + \ 'gotags': ['github.com/jstemmer/gotags@master'], + \ 'guru': ['golang.org/x/tools/cmd/guru@master'], + \ 'impl': ['github.com/josharian/impl@master'], + \ 'keyify': ['honnef.co/go/tools/cmd/keyify@master'], + \ 'motion': ['github.com/fatih/motion@master'], + \ 'iferr': ['github.com/koron/iferr@master'], \ } " These commands are available on any filetypes @@ -90,22 +80,21 @@ function! s:GoInstallBinaries(updateBinaries, ...) endif if go#path#Default() == "" - echohl Error - echomsg "vim.go: $GOPATH is not set and 'go env GOPATH' returns empty" - echohl None + call go#util#EchoError('$GOPATH is not set and `go env GOPATH` returns empty') return endif let go_bin_path = go#path#BinPath() - " change $GOBIN so go get can automatically install to it - let $GOBIN = go_bin_path + let [l:goos, l:goarch] = go#util#hostosarch() + let Restore_goos = go#util#SetEnv('GOOS', l:goos) + let Restore_goarch = go#util#SetEnv('GOARCH', l:goarch) - " old_path is used to restore users own path - let old_path = $PATH + " change $GOBIN so go get can automatically install to it + let Restore_gobin = go#util#SetEnv('GOBIN', go_bin_path) " vim's executable path is looking in PATH so add our go_bin path to it - let $PATH = go_bin_path . go#util#PathListSep() . $PATH + let Restore_path = go#util#SetEnv('PATH', go_bin_path . go#util#PathListSep() . $PATH) " when shellslash is set on MS-* systems, shellescape puts single quotes " around the output string. cmd on Windows does not handle single quotes @@ -117,10 +106,7 @@ function! s:GoInstallBinaries(updateBinaries, ...) set noshellslash endif - let l:dl_cmd = ['go', 'get', '-v', '-d'] - if get(g:, "go_get_update", 1) != 0 - let l:dl_cmd += ['-u'] - endif + let l:get_base_cmd = ['go', 'get', '-v'] " Filter packages from arguments (if any). let l:packages = {} @@ -142,50 +128,103 @@ function! s:GoInstallBinaries(updateBinaries, ...) let l:platform = 'windows' endif - for [binary, pkg] in items(l:packages) - let l:importPath = pkg[0] + let l:oldmore = &more + let &more = 0 - let l:run_cmd = copy(l:dl_cmd) - if len(l:pkg) > 1 && get(l:pkg[1], l:platform, '') isnot '' - let l:run_cmd += get(l:pkg[1], l:platform, '') - endif + for [l:binary, l:pkg] in items(l:packages) + let l:importPath = l:pkg[0] - let bin_setting_name = "go_" . binary . "_bin" + " TODO(bc): how to support this with modules? Do we have to clone and then + " install manually? Probably not. I suspect that we can just use GOPATH + " mode and then do the legacy method. + let bin_setting_name = "go_" . l:binary . "_bin" if exists("g:{bin_setting_name}") let bin = g:{bin_setting_name} else if go#util#IsWin() - let bin = binary . '.exe' + let bin = l:binary . '.exe' else - let bin = binary + let bin = l:binary endif endif if !executable(bin) || a:updateBinaries == 1 if a:updateBinaries == 1 - echo "vim-go: Updating " . binary . ". Reinstalling ". importPath . " to folder " . go_bin_path + echo "vim-go: Updating " . l:binary . ". Reinstalling ". importPath . " to folder " . go_bin_path else - echo "vim-go: ". binary ." not found. Installing ". importPath . " to folder " . go_bin_path + echo "vim-go: ". l:binary ." not found. Installing ". importPath . " to folder " . go_bin_path endif - " first download the binary - let [l:out, l:err] = go#util#Exec(l:run_cmd + [l:importPath]) - if l:err - echom "Error downloading " . l:importPath . ": " . l:out + if l:importPath =~ "@" + let Restore_modules = go#util#SetEnv('GO111MODULE', 'on') + let l:tmpdir = go#util#tempdir('vim-go') + let l:cd = exists('*haslocaldir') && haslocaldir() ? 'lcd ' : 'cd ' + let l:dir = getcwd() + try + execute l:cd . fnameescape(l:tmpdir) + let l:get_cmd = copy(l:get_base_cmd) + + if len(l:pkg) > 1 && get(l:pkg[1], l:platform, []) isnot [] + let l:get_cmd += get(l:pkg[1], l:platform, []) + endif + + " TODO(bc): how to install the bin to a different name than the + " binary path? go get does not support -o + " let l:get_cmd += ['-o', printf('%s%s%s', go_bin_path, go#util#PathSep(), bin)] + + let [l:out, l:err] = go#util#Exec(l:get_cmd + [l:importPath]) + if l:err + call go#util#EchoError(printf('Error installing %s: %s', l:importPath, l:out)) + endif + + call call(Restore_modules, []) + finally + execute l:cd . fnameescape(l:dir) + endtry + call call(Restore_modules, []) + else + let l:get_cmd = copy(l:get_base_cmd) + let l:get_cmd += ['-d'] + if get(g:, "go_get_update", 1) != 0 + let l:get_cmd += ['-u'] + endif + + let Restore_modules = go#util#SetEnv('GO111MODULE', 'off') + + " first download the binary + let [l:out, l:err] = go#util#Exec(l:get_cmd + [l:importPath]) + if l:err + call go#util#EchoError(printf('Error downloading %s: %s', l:importPath, l:out)) + endif + + " and then build and install it + let l:build_cmd = ['go', 'build'] + if len(l:pkg) > 1 && get(l:pkg[1], l:platform, []) isnot [] + let l:build_cmd += get(l:pkg[1], l:platform, []) + endif + let l:build_cmd += ['-o', printf('%s%s%s', go_bin_path, go#util#PathSep(), bin), l:importPath] + + let [l:out, l:err] = go#util#Exec(l:build_cmd) + if l:err + call go#util#EchoError(printf('Error installing %s: %s', l:importPath, l:out)) + endif + + call call(Restore_modules, []) endif - " and then build and install it - let l:build_cmd = ['go', 'build', '-o', go_bin_path . go#util#PathSep() . bin, l:importPath] - let [l:out, l:err] = go#util#Exec(l:build_cmd) - if l:err - echom "Error installing " . l:importPath . ": " . l:out + if len(l:pkg) > 2 + call call(get(l:pkg[2], 'after', function('s:noop', [])), []) endif endif endfor " restore back! - let $PATH = old_path + call call(Restore_path, []) + call call(Restore_gobin, []) + call call(Restore_goarch, []) + call call(Restore_goos, []) + if resetshellslash set shellslash endif @@ -195,18 +234,20 @@ function! s:GoInstallBinaries(updateBinaries, ...) else call go#util#EchoInfo('installing finished!') endif + + let &more = l:oldmore endfunction " CheckBinaries checks if the necessary binaries to install the Go tool " commands are available. function! s:CheckBinaries() if !executable('go') - echohl Error | echomsg "vim-go: go executable not found." | echohl None + call go#util#EchoError('go executable not found.') return -1 endif if !executable('git') - echohl Error | echomsg "vim-go: git executable not found." | echohl None + call go#util#EchoError('git executable not found.') return -1 endif endfunction @@ -239,7 +280,15 @@ function! s:register() return endif + let l:RestoreGopath = function('s:noop') + if go#config#AutodetectGopath() + let l:RestoreGopath = go#util#SetEnv('GOPATH', go#path#Detect()) + endif call go#lsp#DidOpen(expand(':p')) + call call(l:RestoreGopath, []) +endfunction + +function! s:noop(...) abort endfunction augroup vim-go diff --git a/pack/acp/start/vim-go/scripts/bench-syntax b/pack/acp/start/vim-go/scripts/bench-syntax old mode 100644 new mode 100755 index 25e89ee..b4496af --- a/pack/acp/start/vim-go/scripts/bench-syntax +++ b/pack/acp/start/vim-go/scripts/bench-syntax @@ -13,7 +13,7 @@ cd "$vimgodir" if [ -z "${1:-}" ]; then echo "unknown version: '${1:-}'" - echo "First argument must be 'vim-7.4', 'vim-8.0', or 'nvim'." + echo "First argument must be 'vim-8.0' or 'nvim'." exit 1 fi diff --git a/pack/acp/start/vim-go/scripts/docker-test b/pack/acp/start/vim-go/scripts/docker-test old mode 100644 new mode 100755 index e984f61..f92ec42 --- a/pack/acp/start/vim-go/scripts/docker-test +++ b/pack/acp/start/vim-go/scripts/docker-test @@ -10,6 +10,6 @@ cd "$vimgodir" docker build --tag vim-go-test . # seccomp=confined is required for dlv to run in a container, hence it's # required for vim-go's debug tests. -docker run --rm --security-opt="seccomp=unconfined" vim-go-test +docker run -e VIMS --rm --security-opt="seccomp=unconfined" vim-go-test # vim:ts=2:sts=2:sw=2:et diff --git a/pack/acp/start/vim-go/scripts/install-tools b/pack/acp/start/vim-go/scripts/install-tools new file mode 100755 index 0000000..0cc322d --- /dev/null +++ b/pack/acp/start/vim-go/scripts/install-tools @@ -0,0 +1,35 @@ +#!/bin/sh +# +# Install and setup a Vim or Neovim for running tests. +# This should work on both GitHub Actions and people's desktop computers, and +# be 100% independent from any system installed Vim. +# + +set -euC + +vimgodir=$(cd -P "$(dirname "$0")/.." > /dev/null && pwd) +cd "$vimgodir" + +vim=${1:-} + +installdir="/tmp/vim-go-test/$1-install" + +# Make sure all Go tools and other dependencies are installed. +echo "Installing Go binaries" +export GOPATH=$installdir +export GO111MODULE=off +export PATH=${GOPATH}/bin:$PATH +"$vimgodir/scripts/run-vim" $vim +':silent :GoUpdateBinaries' +':qa' + +echo "Installing lint tools" +( + mkdir -p "$installdir/share/vim/vimgo/pack/vim-go/start/" + cd "$installdir/share/vim/vimgo/pack/vim-go/start/" + [ -d "vim-vimhelplint" ] || git clone --depth 1 --quiet https://github.com/machakann/vim-vimhelplint + [ -d "vim-vimlparser" ] || git clone --depth 1 --quiet https://github.com/ynkdir/vim-vimlparser + [ -d "vim-vimlint" ] || git clone --depth 1 --quiet https://github.com/syngan/vim-vimlint +) + +echo "vim-go tools installed to: $installdir/share/vim/vimgo/pack/vim-go/start" + +# vim:ts=2:sts=2:sw=2:et diff --git a/pack/acp/start/vim-go/scripts/install-vim b/pack/acp/start/vim-go/scripts/install-vim old mode 100644 new mode 100755 index 709fcca..cc08785 --- a/pack/acp/start/vim-go/scripts/install-vim +++ b/pack/acp/start/vim-go/scripts/install-vim @@ -1,8 +1,8 @@ #!/bin/sh # # Install and setup a Vim or Neovim for running tests. -# This should work on both Travis and people's desktop computers, and be 100% -# independent from any system installed Vim. +# This should work on both GitHub Actions and people's desktop computers, and +# be 100% independent from any system installed Vim. # # It will echo the full path to a Vim binary, e.g.: # /some/path/src/vim @@ -15,28 +15,32 @@ cd "$vimgodir" vim=${1:-} case "$vim" in - "vim-7.4") - tag="v7.4.2009" + "vim-8.0") + # This follows the version in Ubuntu LTS. Vim's master branch isn't always + # stable, and we don't want to have the build fail because Vim introduced a + # bug. + tag="v8.0.1453" giturl="https://github.com/vim/vim" ;; - "vim-8.0") - # This follows the version in Arch Linux. Vim's master branch isn't always - # stable, and we don't want to have the build fail because Vim introduced a - # bug. - tag="v8.0.1542" + "vim-8.2") + # This is the version that's installed by homebrew currently. It doesn't + # have to stay up to date with homebrew, and is only chosen here because + # that's what homebrew was using at the the time and we need a version to + # vimlint with. + tag="v8.2.0200" giturl="https://github.com/vim/vim" ;; "nvim") # Use latest stable version. - tag="v0.3.1" + tag="v0.4.0" giturl="https://github.com/neovim/neovim" ;; *) echo "unknown version: '${1:-}'" - echo "First argument must be 'vim-7.4', 'vim-8.0', or 'nvim'." + echo "First argument must be 'vim-8.0', vim-8.2, or 'nvim'." exit 1 ;; esac @@ -62,7 +66,7 @@ cd "$srcdir" if [ "$1" = "nvim" ]; then # TODO: Use macOS binaries on macOS - curl -Ls https://github.com/neovim/neovim/releases/download/nightly/nvim-linux64.tar.gz | + curl -Ls https://github.com/neovim/neovim/releases/download/$tag/nvim-linux64.tar.gz | tar xzf - -C /tmp/vim-go-test/ mv /tmp/vim-go-test/nvim-linux64 /tmp/vim-go-test/nvim-install mkdir -p "$installdir/share/nvim/runtime/pack/vim-go/start" @@ -88,22 +92,6 @@ else ln -s "$vimgodir" "$installdir/share/vim/vimgo/pack/vim-go/start/vim-go" fi -# Make sure all Go tools and other dependencies are installed. -echo "Installing Go binaries" -export GOPATH=$installdir -export GO111MODULE=off -export PATH=${GOPATH}/bin:$PATH -"$vimgodir/scripts/run-vim" $vim +':silent :GoUpdateBinaries' +':qa' - -echo "Installing lint tools" -( - mkdir -p "$installdir/share/vim/vimgo/pack/vim-go/start/" - cd "$installdir/share/vim/vimgo/pack/vim-go/start/" - [ -d "vim-vimhelplint" ] || git clone --depth 1 --quiet https://github.com/machakann/vim-vimhelplint - [ -d "vim-vimlparser" ] || git clone --depth 1 --quiet https://github.com/ynkdir/vim-vimlparser - [ -d "vim-vimlint" ] || git clone --depth 1 --quiet https://github.com/syngan/vim-vimlint -) - # Don't really need source after successful install. rm -rf "$srcdir" diff --git a/pack/acp/start/vim-go/scripts/lint b/pack/acp/start/vim-go/scripts/lint old mode 100644 new mode 100755 index 553413a..fd06725 --- a/pack/acp/start/vim-go/scripts/lint +++ b/pack/acp/start/vim-go/scripts/lint @@ -11,7 +11,7 @@ cd "$vimgodir" ##################################### if [ -z "${1:-}" ]; then echo "unknown version: '${1:-}'" - echo "First argument must be 'vim-7.4', 'vim-8.0', or 'nvim'." + echo "First argument must be 'vim-8.0' or 'nvim'." exit 1 fi diff --git a/pack/acp/start/vim-go/scripts/run-vim b/pack/acp/start/vim-go/scripts/run-vim old mode 100644 new mode 100755 index 1887807..f5507ee --- a/pack/acp/start/vim-go/scripts/run-vim +++ b/pack/acp/start/vim-go/scripts/run-vim @@ -17,7 +17,7 @@ shift $((OPTIND - 1)) if [ -z "${1:-}" ]; then echo "unknown version: '${1:-}'" - echo "First argument must be 'vim-7.4', 'vim-8.0', or 'nvim'." + echo "First argument must be 'vim-8.0', 'vim-8.2', or 'nvim'." exit 1 fi @@ -34,13 +34,13 @@ fi if [ $coverage -eq 1 ]; then covimerage -q run --report-file /tmp/vim-go-test/cov-profile.txt --append \ - $dir/bin/vim --noplugin -u NONE -N \ + $dir/bin/vim --noplugin -u NONE -i NONE -N \ +"set shm+=WAFI rtp^=$vimgodir packpath=$dir/share/vim/vimgo" \ +'filetype plugin indent on' \ +'packloadall!' \ "$@" else - $dir/bin/vim --noplugin -u NONE -N \ + $dir/bin/vim --noplugin -u NONE -i NONE -N \ +"set shm+=WAFI rtp^=$vimgodir packpath=$dir/share/vim/vimgo" \ +'filetype plugin indent on' \ +'packloadall!' \ diff --git a/pack/acp/start/vim-go/scripts/runtest.vim b/pack/acp/start/vim-go/scripts/runtest.vim index 9d20b76..3b16723 100644 --- a/pack/acp/start/vim-go/scripts/runtest.vim +++ b/pack/acp/start/vim-go/scripts/runtest.vim @@ -20,6 +20,7 @@ if !exists('g:test_verbose') let g:test_verbose = 0 endif let g:go_echo_command_info = 0 +let g:go_gopls_options = [] function! s:logmessages() abort " Add all messages (usually errors). @@ -64,29 +65,43 @@ for s:test in sort(s:tests) endif try exe 'call ' . s:test + " sleep to give events a chance to be processed. This is especially + " important for the LSP code to have a chance to run before Vim exits, in + " order to avoid errors trying to write to the gopls channels since Vim + " would otherwise stop gopls before the event handlers were run and result + " in 'stream closed' errors when the events were run _after_ gopls exited. + sleep 50m catch let v:errors += [v:exception] endtry + let s:elapsed_time = substitute(reltimestr(reltime(s:started)), '^\s*\(.\{-}\)\s*$', '\1', '') + " Restore GOPATH after each test. let $GOPATH = s:gopath " Restore the working directory after each test. execute s:cd . s:dir - let s:elapsed_time = substitute(reltimestr(reltime(s:started)), '^\s*\(.\{-}\)\s*$', '\1', '') - let s:done += 1 + try + " exit gopls after each test + call go#lsp#Exit() + catch /^Vim(call):E900: Invalid channel id/ + " do nothing - gopls has stopped + endtry - call s:logmessages() + let s:done += 1 if len(v:errors) > 0 let s:fail += 1 call add(s:logs, printf("--- FAIL %s (%ss)", s:test[:-3], s:elapsed_time)) + call s:logmessages() call extend(s:logs, map(v:errors, '" ". v:val')) " Reset so we can capture failures of the next test. let v:errors = [] else if g:test_verbose is 1 + call s:logmessages() call add(s:logs, printf("--- PASS %s (%ss)", s:test[:-3], s:elapsed_time)) endif endif diff --git a/pack/acp/start/vim-go/scripts/test b/pack/acp/start/vim-go/scripts/test old mode 100644 new mode 100755 index 04e5169..77dd9f8 --- a/pack/acp/start/vim-go/scripts/test +++ b/pack/acp/start/vim-go/scripts/test @@ -14,19 +14,22 @@ _usage() { echo " -h Show this help" echo " -v Enable verbose output" echo " -r Run only the tests from this file" - echo " -c Generate and submit code coverage reports" + echo " -c Generate code coverage reports" + echo " -u Submit code coverage reports" echo } verbose=0 run="" coverage="" -while getopts "hvcr:" option; do +uploadcoverage="" +while getopts "hvcur:" option; do case "$option" in h) _usage; exit 0 ;; v) verbose=1; ;; r) run=$OPTARG ;; c) coverage="-c" ;; + u) uploadcoverage=1 ;; *) echo "error: unknown option '$option'" _usage @@ -40,7 +43,7 @@ shift $((OPTIND - 1)) ##################################### if [ -z "${1:-}" ]; then echo "unknown version: '${1:-}'" - echo "First argument must be 'vim-7.4', 'vim-8.0', or 'nvim'." + echo "First argument must be 'vim-8.0' or 'nvim'." exit 1 fi @@ -69,6 +72,7 @@ find "$vimgodir" -name '*_test.vim' | while read test_file; do "$vimgodir/scripts/run-vim" $coverage $vim -e \ +"silent e $test_file" \ +"let g:test_verbose=$verbose" \ + +"let g:go_echo_command_info=0" \ -S ./scripts/runtest.vim < /dev/null || ( # If Vim exits with non-0 it's almost certainly a bug in the test runner; # should very rarely happen in normal usage. @@ -90,12 +94,15 @@ if [ -f "/tmp/vim-go-test/FAILED" ]; then fi echo 2>&1 "All ($vim) tests PASSED" -# Submit coverage reports +# Generate coverage reports if [ -n "$coverage" ]; then coverage xml --omit '*_test.vim' - codecov -X search gcov pycov -f coverage.xml --required \ + + if [ -n "$uploadcoverage" ]; then + codecov -X search gcov pycov -f coverage.xml --required \ --flags "$(echo "$vim" | sed -s 's/[-.]//g')" - rm coverage.xml + rm coverage.xml + fi fi # vim:ts=2:sts=2:sw=2:et diff --git a/pack/acp/start/vim-go/syntax/go.vim b/pack/acp/start/vim-go/syntax/go.vim index 04af07b..9b64075 100644 --- a/pack/acp/start/vim-go/syntax/go.vim +++ b/pack/acp/start/vim-go/syntax/go.vim @@ -117,7 +117,7 @@ if go#config#HighlightFormatStrings() \@<=%[-#0 +]*\ \%(\%(\%(\[\d\+\]\)\=\*\)\|\d\+\)\=\ \%(\.\%(\%(\%(\[\d\+\]\)\=\*\)\|\d\+\)\=\)\=\ - \%(\[\d\+\]\)\=[vTtbcdoqxXUeEfFgGsp]/ contained containedin=goString,goRawString + \%(\[\d\+\]\)\=[vTtbcdoqxXUeEfFgGspw]/ contained containedin=goString,goRawString hi def link goFormatSpecifier goSpecialString endif @@ -162,15 +162,23 @@ endif syn match goSingleDecl /\%(import\|var\|const\) [^(]\@=/ contains=goImport,goVar,goConst " Integers -syn match goDecimalInt "\<-\=\d\+\%([Ee][-+]\=\d\+\)\=\>" -syn match goHexadecimalInt "\<-\=0[xX]\x\+\>" -syn match goOctalInt "\<-\=0\o\+\>" -syn match goOctalError "\<-\=0\o*[89]\d*\>" +syn match goDecimalInt "\<-\=\(0\|[1-9]_\?\(\d\|\d\+_\?\d\+\)*\)\%([Ee][-+]\=\d\+\)\=\>" +syn match goDecimalError "\<-\=\(_\(\d\+_*\)\+\|\([1-9]\d*_*\)\+__\(\d\+_*\)\+\|\([1-9]\d*_*\)\+_\+\)\%([Ee][-+]\=\d\+\)\=\>" +syn match goHexadecimalInt "\<-\=0[xX]_\?\(\x\+_\?\)\+\>" +syn match goHexadecimalError "\<-\=0[xX]_\?\(\x\+_\?\)*\(\([^ \t0-9A-Fa-f_]\|__\)\S*\|_\)\>" +syn match goOctalInt "\<-\=0[oO]\?_\?\(\o\+_\?\)\+\>" +syn match goOctalError "\<-\=0[0-7oO_]*\(\([^ \t0-7oOxX_/)\]\}\:]\|[oO]\{2,\}\|__\)\S*\|_\|[oOxX]\)\>" +syn match goBinaryInt "\<-\=0[bB]_\?\([01]\+_\?\)\+\>" +syn match goBinaryError "\<-\=0[bB]_\?[01_]*\([^ \t01_]\S*\|__\S*\|_\)\>" hi def link goDecimalInt Integer +hi def link goDecimalError Error hi def link goHexadecimalInt Integer +hi def link goHexadecimalError Error hi def link goOctalInt Integer hi def link goOctalError Error +hi def link goBinaryInt Integer +hi def link goBinaryError Error hi def link Integer Number " Floating point @@ -382,6 +390,31 @@ hi def link goCoverageNormalText Comment function! s:hi() hi def link goSameId Search + hi def link goDiagnosticError SpellBad + hi def link goDiagnosticWarning SpellRare + + " TODO(bc): is it appropriate to define text properties in a syntax file? + " The highlight groups need to be defined before the text properties types + " are added, and when users have syntax enabled in their vimrc after + " filetype plugin on, the highlight groups won't be defined when + " ftplugin/go.vim is executed when the first go file is opened. + " See https://github.com/fatih/vim-go/issues/2658. + if has('textprop') + if empty(prop_type_get('goSameId')) + call prop_type_add('goSameId', {'highlight': 'goSameId'}) + endif + if empty(prop_type_get('goDiagnosticError')) + call prop_type_add('goDiagnosticError', {'highlight': 'goDiagnosticError'}) + endif + if empty(prop_type_get('goDiagnosticWarning')) + call prop_type_add('goDiagnosticWarning', {'highlight': 'goDiagnosticWarning'}) + endif + endif + + hi def link goDeclsFzfKeyword Keyword + hi def link goDeclsFzfFunction Function + hi def link goDeclsFzfSpecialComment SpecialComment + hi def link goDeclsFzfComment Comment " :GoCoverage commands hi def goCoverageCovered ctermfg=green guifg=#A6E22E @@ -389,8 +422,8 @@ function! s:hi() " :GoDebug commands if go#config#HighlightDebug() - hi GoDebugBreakpoint term=standout ctermbg=117 ctermfg=0 guibg=#BAD4F5 guifg=Black - hi GoDebugCurrent term=reverse ctermbg=12 ctermfg=7 guibg=DarkBlue guifg=White + hi def GoDebugBreakpoint term=standout ctermbg=117 ctermfg=0 guibg=#BAD4F5 guifg=Black + hi def GoDebugCurrent term=reverse ctermbg=12 ctermfg=7 guibg=DarkBlue guifg=White endif endfunction diff --git a/pack/acp/start/vim-go/syntax/gomod.vim b/pack/acp/start/vim-go/syntax/gomod.vim index 5e1e3a1..7dcef1a 100644 --- a/pack/acp/start/vim-go/syntax/gomod.vim +++ b/pack/acp/start/vim-go/syntax/gomod.vim @@ -9,17 +9,20 @@ syntax case match " match keywords syntax keyword gomodModule module +syntax keyword gomodGo go contained syntax keyword gomodRequire require syntax keyword gomodExclude exclude syntax keyword gomodReplace replace -" require, exclude and replace can be also grouped into block +" require, exclude, replace, and go can be also grouped into block syntax region gomodRequire start='require (' end=')' transparent contains=gomodRequire,gomodVersion syntax region gomodExclude start='exclude (' end=')' transparent contains=gomodExclude,gomodVersion syntax region gomodReplace start='replace (' end=')' transparent contains=gomodReplace,gomodVersion +syntax match gomodGo '^go .*$' transparent contains=gomodGo,gomodGoVersion " set highlights highlight default link gomodModule Keyword +highlight default link gomodGo Keyword highlight default link gomodRequire Keyword highlight default link gomodExclude Keyword highlight default link gomodReplace Keyword @@ -36,6 +39,10 @@ highlight default link gomodString String syntax match gomodReplaceOperator "\v\=\>" highlight default link gomodReplaceOperator Operator +" match go versions +syntax match gomodGoVersion "1\.\d\+" contained +highlight default link gomodGoVersion Identifier + " highlight versions: " * vX.Y.Z-pre