From dc8d70dc6a1e27b0ce5ac2eb3832ac2fd961a639 Mon Sep 17 00:00:00 2001 From: ray-x Date: Fri, 7 Nov 2025 15:43:49 +1100 Subject: [PATCH 01/21] adding gopls cmds routing and update doc --- README.md | 2 ++ lua/go.lua | 7 +++++-- lua/go/gopls.lua | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index dd24042ca..fe927f8af 100644 --- a/README.md +++ b/README.md @@ -850,6 +850,8 @@ require('go').setup({ -- signs = {'ξͺ‡', '', 'ξ©΄', 'ο„©'}, -- set to true to use default signs, an array of 4 to specify custom signs -- update_in_insert = false, -- }, + -- set to false/nil: disable config gopls diagnostic + -- if you need to setup your ui for input and select, you can do it here -- go_input = require('guihua.input').input -- set to vim.ui.input to disable guihua input -- go_select = require('guihua.select').select -- vim.ui.select to disable guihua select diff --git a/lua/go.lua b/lua/go.lua index e71db61d7..07cd8de2c 100644 --- a/lua/go.lua +++ b/lua/go.lua @@ -74,6 +74,8 @@ _GO_NVIM_CFG = { severity = vim.diagnostic.severity.WARN, -- severity level of the diagnostics }, }, + diagnostic = false, -- set to false to disable diagnostic setup from go.nvim + --[[ diagnostic = { -- set diagnostic to false to disable diagnostic hdlr = false, -- hook diagnostic handler and send error to quickfix underline = true, @@ -84,7 +86,7 @@ _GO_NVIM_CFG = { -- signs = { -- text = { 'πŸš‘', 'πŸ”§', 'πŸͺ›', '🧹' }, -- }, - }, + }, --]] go_input = function() if require('go.utils').load_plugin('guihua.lua', 'guihua.gui') then return require('guihua.input').input @@ -274,9 +276,10 @@ function go.setup(cfg) else -- we do not setup diagnostic from go.nvim -- use whatever user has setup - _GO_NVIM_CFG.diagnostic = {} + _GO_NVIM_CFG.diagnostic = nil end else + -- vim.notify('go.nvim diagnostic setup deprecated, use vim.diagnostic instead', vim.log.levels.DEBUG) local dcfg = vim.tbl_extend('force', {}, _GO_NVIM_CFG.diagnostic) vim.diagnostic.config(dcfg) end diff --git a/lua/go/gopls.lua b/lua/go/gopls.lua index beca2bec9..d6c7ddaa1 100644 --- a/lua/go/gopls.lua +++ b/lua/go/gopls.lua @@ -27,10 +27,12 @@ local gopls_cmds = { 'gopls.gc_details', 'gopls.generate', 'gopls.go_get_package', + 'gopls.lsp', 'gopls.list_imports', 'gopls.list_known_packages', 'gopls.maybe_prompt_for_telemetry', 'gopls.mem_stats', + 'gopls.modify_tags', 'gopls.modules', 'gopls.package_symbols', 'gopls.packages', @@ -41,6 +43,7 @@ local gopls_cmds = { 'gopls.run_govulncheck', 'gopls.run_tests', 'gopls.scan_imports', + 'gopls.split_package', 'gopls.start_debugging', 'gopls.start_profile', 'gopls.stop_profile', From 364cedb19473781775ddda6dea30d7e0f01ad9de Mon Sep 17 00:00:00 2001 From: ray-x Date: Mon, 2 Mar 2026 15:34:33 +1100 Subject: [PATCH 02/21] Add GoAI command --- doc/go.txt | 634 +++++++++++++++++++++++++++++--------------- lua/go/ai.lua | 553 ++++++++++++++++++++++++++++++++++++++ lua/go/commands.lua | 42 +++ lua/go/comment.lua | 80 ++++++ lua/go/gopls.lua | 70 +++++ lua/go/gotest.lua | 2 +- 6 files changed, 1173 insertions(+), 208 deletions(-) create mode 100644 lua/go/ai.lua diff --git a/doc/go.txt b/doc/go.txt index 58748b47a..c479333bf 100644 --- a/doc/go.txt +++ b/doc/go.txt @@ -120,42 +120,119 @@ go ~ *go-nvim-compiler-gotest* gotest~ - with `go test`, `richgo`, or `ginkgo` + with `go test`, `richgo`, `gotestsum`, or `ginkgo` *go-nvim-compiler-golint* golint~ - with golangcl-lint + with golangci-lint ============================================================================== COMMANDS *go-nvim-commands* +INSTALL & UPDATE ~ + :GoInstallBinaries *:GoInstallBinaries* Make sure all dependent tools are downloaded and installed. -:GoUpdateBinaries *:GoUpdataBinaries* +:GoUpdateBinaries *:GoUpdateBinaries* Make sure all tools are updated. +:GoInstallBinary {tool_name} *:GoInstallBinary* + Install a specific tool binary. -:GoInstallBinary {tool_name} *:GoInstallBinary* - Make sure all dependent tools are downloaded and installed. +:GoUpdateBinary {tool_name} *:GoUpdateBinary* + Update a specific tool binary. + +BUILD, RUN & LINT ~ + +:GoMake *:GoMake* + Async make for the current project. + +:GoBuild {-tags=tagname} {package_name} *:GoBuild* + Build the package in the current directory. (|:pwd|) + %: build current file + %:h: build current package + +:GoRun {args} *:GoRun* + Equal to "go run" with args. You can also specify -F to run in + floaterm. + +:GoGenerate {args} *:GoGenerate* + Run `go generate`. Args: package path. + +:GoStop {job} *:GoStop* + Stop a running async job started with GoRun or other async commands. + +:GoVet {args} *:GoVet* + Run `go vet`. Args: package path. -:GoUpdateBinary {tool_name} *:GoUpdataBinary* - Make sure tool_name are up to date. +:GoLint {args} *:GoLint* + Run golangci-lint. Configured via |golangci_lint| option. +:GoTool {subcommand} *:GoTool* + Run `go tool `. Tab completion for available tools. -:GoCoverage [flags] *:GoCoverage* +TEST ~ + +:GoTest {-cnfptvFba} {-t tagname} {package_name} *:GoTest* + Run tests with various options: + -c: compile test in current package + -n: test nearest function at cursor + -v: verbose mode + -f: test current file + -p: test current package + -t: build tag + -F: run in floaterm + -b {build_args}: e.g. `-b -gcflags="all -N -l"` + -a {args}: extra args for test, check `go help testflag` + -parallel {N}: run tests in parallel + package_name: test specific package + +:GoTestFunc {args} {-t tagname} *:GoTestFunc* + Test the function under cursor. If cursor is not on a test function, + shows a selector to pick one. + -s: select the function you want to run + -v: verbose mode + +:GoTestSubCase *:GoTestSubCase* + Test the table-driven test sub-case under cursor. + +:GoTestFile {-t tagname} {-v} *:GoTestFile* + Test all functions in the current file. + -v: verbose mode + +:GoTestPkg {args} *:GoTestPkg* + Test all tests in the current package. + +:GoTestSum {packagename} {-w} {gotestsum_args} *:GoTestSum* + Run tests with gotestsum. + -w: watch file changes and re-run test + packagename: test package + gotestsum_args: gotestsum arguments, + e.g. GoTestSum -- -tags=integration ./io/http + +:GoAddTest *:GoAddTest* + Generate unit test for the function under cursor (using gotests). + +:GoAddExpTest {args} *:GoAddExpTest* + Generate tests for all exported functions in the current file. + +:GoAddAllTest {args} *:GoAddAllTest* + Generate tests for all functions in the current file. + +COVERAGE ~ + +:GoCoverage [flags] *:GoCoverage* Run `go test -cover` and highlight lines reported as covered and uncovered. - [flags] are passed to the `go test` command; there are two special - flags: + [flags] are passed to the `go test` command; special flags: -t coveragefile load coverage data from coveragefile - -p Coverage for current package only -r Remove existing highlighting in current buffer. -R Remove all existing highlighting. - -t Toggle display of highlighting. + -f Toggle display of highlighting. -m show coverage statistics in qf. *hl-goCoverageCovered* *hl-goCoverageUncover* @@ -163,273 +240,416 @@ COMMANDS *go-nvim-commands* Override the goCoverageCovered and/or goCoverageUncover highlight groups if the defaults don't work well for you. augroup my-go-nvim-coverage - au! au Syntax go hi goCoverageCovered guibg=green au Syntax go hi goCoverageUncover guibg=brown augroup end -:GoImports {package_name} *:GoImports* - Add, modify imports. +FORMAT & IMPORTS ~ -:GoBuild {-tags=tagname}{package_name} *:GoBuild* - Build the package in the current directory. (|:pwd|) - -g: build with debug info - %: build current file - %:h: build current package +:GoFmt {formatter} *:GoFmt* + Format code. Optional arg: gofmt, goimports, gofumpt, golines. + Default configured via |gofmt| option. -:GoRun {args} *:GoRun* - equal to "go run " with args. You can also specify -F to run in floaterm +:GoImports {package_name} *:GoImports* + Add, modify imports. Tab completion for package names. + Uses gopls by default (configurable via |goimports| option). -:GoStop *:GoStop* - stop the task started with GoRun -:GoTest {-cnfpt} {-t tagname} {package_name} {-a args} *:GoTest* - -c: compile test in current package - -n: test nearest - -v: test verbose mode - -f: test current file - -p: test current package - -t: compile tag - -a: extra args for test, check `go help testflag` concatenate - with '\ ' if there are any spaces or multiple args - -b {build_args}: e.g. `-b -gcflags="all-N\ -l"` - -n {count}: disable cache -count={count} - package_name: test package - -:GoTestSum {packagename} {-w} {gotestsum_args} *:GoTestSum* - -w: watch file changes and re-run test - packagename: test package - gottestsum_args: gotestsum arguments, - e.g. GoTestSum -- -tags=integration ./io/http +CODE GENERATION ~ +:GoIfErr *:GoIfErr* + Generate `if err != nil` boilerplate at cursor. -:GoTestFile {-t tagname}{-v} *:GoTestFile* - Test current file - -v: verbose mode +:GoFillStruct *:GoFillStruct* + Fill struct literal with default field values. -:GoTestFunc {args} {-t tagname} *:GoTestFunc* - Test current function - {args} -s: select the function you want to run - -v: verbose mode +:GoFillSwitch *:GoFillSwitch* + Fill switch statement with all type/enum cases. -:GoAddTest *:GoAddTest* - Add unit test for current function +:GoFixPlurals *:GoFixPlurals* + Merge consecutive same-type function parameters. -:GoFmt {-a} *:GoFmt* - Format code with golines+gofumpt. -a: apply to all files -:GoVet *:GoVet* - Run go vet -:GoCheat query *:GoCheat* - Run `curl cheat.sh/go/query` -:GoGet {package_url} *:GoGet* - Run go get {package_url}, if package_url not provided, will parse - current line and use it as url if valid +:GoCmt *:GoCmt* + Generate doc comment for function/struct/interface under cursor. -:GoLint *:GoLint* - Run golangci-lint +:GoCmtAI *:GoCmtAI* + Generate doc comment using AI/Copilot for the declaration at cursor. + Uses the configured AI provider (see |ai| option). +:GoImpl {receiver} {interface} *:GoImpl* + Generate interface method stubs. + If cursor is on a struct name, receiver can be omitted. + If cursor is on an interface name, interface can be omitted. + Examples: > + :GoImpl io.Reader + :GoImpl f *File io.Reader + :GoImpl f io.Reader + :GoImpl MyType + :GoImpl mt MyType +< + Tab completion for interface names. -:GoRename *:GoRename* - Rename the identifier under the cursor. +:GoEnum {args} *:GoEnum* + Generate enum helpers for the type under cursor. -:Gomvp *:Gomvp* - Rename the module name under the cursor. +:GoGenReturn *:GoGenReturn* + Generate return values for the function call under cursor. -:{range}GoAddTag [flags] *:GoAddTags* - Add, modify, or remove struct tags. Will only apply to the fields in - {range} if it's given, or applied to all fields in the struct if it's - omitted. +:["x]GoJson2Struct {name} *:GoJson2Struct* + Convert JSON (visual selection or register) to Go struct. + Supports visual range, bang to put result to register, + "x to get JSON from register x. - All tags in [flags] will be added. A tag can be followed by a `,` - (comma) and an option to add the option, or set to a specific name - with `tag:name`. +STRUCT TAGS ~ - Tags can be removed by using `-rm tag`; options can be removed by - using `-rm tag,opt` +:{range}GoAddTag [tags] *:GoAddTag* + Add struct tags. Will only apply to the fields in {range} if given, + or applied to all fields in the struct if omitted. - The value of |g:go-nvim_tag_default| is used if no [flags] is given. + Tags can include options: `json,omitempty`. Multiple tags separated + by spaces. - options: - -transform (-t) - -add-options (-a) {opts} Examples: > - :GoTag json Add tag "json" - :GoTag json,omitempty Add tag with omitempty, or add - omitempty for fields where it - already exists. - :GoTag json,omitempty db Add two tags - :GoAddTags sometag:foo Set the tag sometag to the - string foo. - :GoTags json -rm yaml Combine add and rm + :GoAddTag json Add tag "json" + :GoAddTag json,omitempty Add with omitempty + :GoAddTag json,omitempty db Add two tags < -:{range}GoRmTag [flags] *:GoRmTags* - Remove struct tags. Will apply to the fields in - {range} if it's given, or applied to all fields in the struct if it's - omitted. - +:{range}GoRmTag [tags] *:GoRmTag* + Remove struct tags. Examples: > - :GoRmTag json Remove a tag - :GoRmTag json,omitempty Remove the omitempty option - :GoRmTag json -rm db Remove two tags + :GoRmTag json Remove json tag + :GoRmTag json,omitempty Remove omitempty option :GoRmTag Remove all tags +< + +:GoClearTag *:GoClearTag* + Remove all struct tags from the struct under cursor. + +:{range}GoModifyTag [tags] *:GoModifyTag* + Modify struct tag options (e.g. transform to snakecase/camelcase). + Tab completion for options. + +MODULE MANAGEMENT ~ + +:GoModTidy {args} *:GoModTidy* + Run `go mod tidy`. Without args, uses gopls; with args, runs + `go mod tidy` directly. + +:GoModVendor {args} *:GoModVendor* + Run `go mod vendor`. + +:GoModDnld {args} *:GoModDnld* + Run `go mod download`. + +:GoModGraph {args} *:GoModGraph* + Show module dependency graph. + +:GoModWhy {module} *:GoModWhy* + Show why a module is needed. If no arg, uses module under cursor. + +:GoModInit {name} *:GoModInit* + Initialize new Go module. + +:GoGet {package_url} *:GoGet* + Run `go get {package_url}`. If not provided, will parse current line + and use it as URL if valid. + +:GoWork {cmd} {path} *:GoWork* + Go workspace commands. Subcommands: run, use. -:{range}GoClearTag *:GoClearTags* - Remove all tags +NAVIGATION & DOCS ~ -:GoDebug {options} *:GoDebug* - Start debugger - options: -t(test), -R(restart), -n(nearest), -f(file), -s(stop), -b(breakpoint) - -h(help), -c(compile), -a (attach remote) - If no option provided, will - 1) check launch.json and launch the valid configuration from - launch.json, fallback to GoDebug file - 2) With -t option, if current file is not test file, will switch to test file - and run test for current function - 3) If cursor inside scope of a test function, will debug current test function, - if cursor inside a test file, will debug current test file +:GoAlt *:GoAlt* + Open alternative file (test <-> implementation). + Supports bang (!) to create if not exists. Also GoAltS/GoAltV. -:GoDbgConfig *:GoDbgConfig* - Open launch.json +:GoDoc {symbol} *:GoDoc* + Show documentation. e.g. `GoDoc fmt.Println` -:GoDbgKeys *:GoDbgKeys* - Display keymaps for debugger +:GoDocBrowser {symbol} *:GoDocBrowser* + Open doc for current function/type/package in browser. + e.g. `GoDocBrowser fmt.Println` -:GoDbgStop *:GoDbgStop* - Stop debug session and unmap all keymaps, same as GoDebug -s +:GoImplements *:GoImplements* + Show interface implementations via `vim.lsp.buf.implementation()`. -:GoDbgContinue *:GoDbgContinue* - Continue debug session, keymap `c` +:GoPkgOutline {options} *:GoPkgOutline* + Show symbols inside a package in side panel/loclist. + Options: -f (floating win), package_name + Default: sidepanel, current package. -:GoCreateLaunch *:GoCreateLaunch* - Create a sample launch.json configuration file +:GoPkgSymbols *:GoPkgSymbols* + Show symbols inside current package in side panel/loclist. -:GoBreakToggle *:GoBreakToggle* - Debugger breakpoint toggle +:GoListImports *:GoListImports* + List imports in the current file in a floating window. -:GoBreakSave *:GoBreakSave* - Debugger breakpoint save to project file +:GoCheat {query} *:GoCheat* + Run `curl cheat.sh/go/{query}`. -:GoBreakLoad *:GoBreakLoad* - Debugger breakpoint load from project file +CODE ACTIONS & REFACTORING ~ -:GoEnv {envfile} {load} *:GoEnv* - Load envfile and set environment variable +:{range}GoCodeAction *:GoCodeAction* + Run LSP code actions. Supports visual range. -:GoAlt *:GoAlt* - Open alternative file (test/go), Also GoAltS/GoAltV +:GoCodeLenAct *:GoCodeLenAct* + Run code lens action at cursor. -:GoDoc {options} *:GoDoc* - e.g. GoDoc fmt.Println +:GoRename *:GoRename* + Rename the identifier under the cursor via LSP. -:GoDocBrowser {options} *:GoDocBrowser* - Open doc current function/type/package in browser - e.g. GoDocBrowser fmt.Println +:GoGCDetails {args} *:GoGCDetails* + Toggle GC optimization details overlay for the current file. -:GoMockGen {options} *:GoMockGen* - Generate mock with go mock - options: - -s source mode(default) - -i interface mode, provide interface name or put cursor on interface - -p package name default: mocks +:GoToggleInlay *:GoToggleInlay* + Toggle LSP inlay hints for the current buffer. + +MOCK ~ + +:GoMockGen {options} *:GoMockGen* + Generate mock with gomock/mockgen. + Options: + -s source mode (default) + -i interface mode, provide interface name or put cursor on + interface + -p package name, default: mocks -d destination directory, default: ./mocks -:GoPkgOutline {options} *:GoPkgOutline* - show symbols inside a specific package in side panel/loclist - options: -f (floating win), -p package_name - default options: sidepanel, current package in vim buffer +DEBUGGING ~ + *go-debug* +Debugging requires nvim-dap. Enable with `dap_debug = true` (default). + +:GoDebug {options} *:GoDebug* + Start debugger. + Options: -t (test), -R (restart), -n (nearest), -f (file), + -s (stop), -b (breakpoint), -h (help), -c (compile), -a (attach) + If no option provided, will: + 1) Check launch.json and launch the valid configuration, + fallback to GoDebug file + 2) With -t option, if current file is not test file, will switch + to test file and run test for current function + 3) If cursor inside scope of a test function, will debug current + test function + +:GoDbgConfig *:GoDbgConfig* + Open launch.json. + +:GoCreateLaunch *:GoCreateLaunch* + Create a sample launch.json configuration file. + +:GoDbgKeys *:GoDbgKeys* + Display keymaps for debugger. + +:GoDbgStop *:GoDbgStop* + Stop debug session and unmap all keymaps. Same as `GoDebug -s`. + +:GoDbgContinue *:GoDbgContinue* + Continue debug session (keymap `c`). + +:GoBreakToggle *:GoBreakToggle* + Toggle breakpoint at cursor line. + +:GoBreakSave *:GoBreakSave* + Save breakpoints to project file. + +:GoBreakLoad *:GoBreakLoad* + Load breakpoints from project file. + +:BreakCondition *:BreakCondition* + Set a conditional breakpoint (prompts for condition). -:GoPkgSymbols *:GoPkgSymbols* - show symbols inside current package in side panel/loclist - options: none +:LogPoint *:LogPoint* + Set a log point breakpoint (prompts for log message). -:GoImplements {options} *:GoImplements* - GoImplements calls vim.lsp.buf.implementation +:DapStop *:DapStop* + Stop the current DAP session. -:GoImpl {options} *:GoImpl* - e.g. GoImpl {receiver} {interface}, will check if cursor is a valid - receiver, if you park cursor on struct name, receiver can be omitted. - if you park cursor on interface name, {interface} can be omitted. - e.g ":GoImpl io.Reader", or "GoImpl f *File io.Reader" or "GoImpl - f io.Reader", or "GoImpl MyType", "GoImpl mt MyType" - you can use tab to complete the interface name. +:DapRerun *:DapRerun* + Disconnect, close, and rerun the last DAP session. -:GoToggleInlay - Toggle inlay hints for current buffer +:DapUiFloat *:DapUiFloat* + Open dap-ui floating element. -:GoTermClose - Closes the floating term. +:DapUiToggle *:DapUiToggle* + Toggle dap-ui. -:["x]GoJson2Struct - Convert json (visual select) to go struct. - bang: put result to register - \"x : get json from register x +:ReplRun *:ReplRun* + Run last command in DAP REPL. -:GoGenReturn - generate return values for current function +:ReplToggle *:ReplToggle* + Toggle DAP REPL. -:GoVulnCheck - run govulncheck on current project +:ReplOpen *:ReplOpen* + Open DAP REPL in a split. -:GoEnum - run goenum on current file +GINKGO ~ -:GoNew {filename} - create a new go file from template e.g. GoNew ./pkg/file.go +:Ginkgo {cmd} *:Ginkgo* + Ginkgo test framework commands. + Subcommands: generate, bootstrap, build, labels, run, watch. + +:GinkgoFunc {args} *:GinkgoFunc* + Run Ginkgo test for the function under cursor. + +:GinkgoFile {args} *:GinkgoFile* + Run Ginkgo tests for the current file. + +OTHER ~ + +:GoEnv {envfile} *:GoEnv* + Load environment variables from file. + +:GoProject *:GoProject* + Setup project configuration. Loads .gonvim/init.lua if present. + +:GoVulnCheck {args} *:GoVulnCheck* + Run govulncheck for vulnerability scanning on current project. + +:GoNew {template} *:GoNew* + Create a new Go file from template. + e.g. `GoNew ./pkg/file.go` + Tab completion for available templates. + +:Gomvp {args} *:Gomvp* + Rename/move packages. Tab completion for package names. + +:GoTermClose *:GoTermClose* + Close the floating terminal (when run_in_floaterm is enabled). + +GOPLS LSP COMMANDS ~ + +:GoGopls {subcommand} {json_args} *:GoGopls* + Execute any gopls LSP command directly. + Tab completion for available subcommands. + Arguments can be JSON or key=value pairs. + Examples: > + :GoGopls tidy + :GoGopls add_import {"ImportPath":"fmt"} + :GoGopls list_known_packages + :GoGopls gc_details +< + + Available subcommands include: + add_dependency, add_import, add_test, apply_fix, assembly, + change_signature, check_upgrades, diagnose_files, doc, + edit_go_directive, extract_to_new_file, free_symbols, + gc_details, generate, go_get_package, list_imports, + list_known_packages, mem_stats, modify_tags, modules, + package_symbols, packages, regenerate_cgo, remove_dependency, + reset_go_mod_diagnostics, run_go_work_command, run_govulncheck, + run_tests, scan_imports, split_package, tidy, update_go_sum, + upgrade_dependency, vendor, vulncheck, workspace_stats + +AI-POWERED ~ + +:GoAI {natural_language_request} *:GoAI* + Translate a natural language request into the correct go.nvim + command using an LLM (Copilot or OpenAI-compatible). + Supports visual range β€” range is forwarded to range-capable commands. + If no argument is given, opens an interactive prompt. + Examples: > + :GoAI run unit test for tags test + :GoAI format file with gofumpt + :GoAI add json tags to struct + :'<,'>GoAI convert this json to a go struct +< + Configure via the |ai| option. ============================================================================== OPTIONS *go-nvim-options* You can setup go.nvim with following options: - +> { - goimports = "gopls", -- if set to 'gopls' will use gopls format, also goimports - gofmt = "gofumpt", -- if set to gopls will use gopls format - max_line_len = 120, - tag_transform = false, - test_dir = "", + disable_defaults = false, -- true: disable all default settings + remap_commands = {}, -- remap or disable commands, e.g. {GoFmt="GoFormat", GoDoc=false} + go = "go", -- go binary, e.g. "go1.21" + goimports = "gopls", -- "gopls" uses gopls format, also "goimports" + fillstruct = "gopls", -- "gopls" or "fillstruct" + gofmt = "gopls", -- "gopls", "gofumpt", "gofmt", "golines" + max_line_len = 0, -- max line length for golines (0 = disabled) + tag_transform = false, -- gomodifytags transform, e.g. "snakecase" + tag_options = "json=omitempty", -- gomodifytags default tag options + gotests_template = "", -- gotests -template parameter + gotests_template_dir = "", -- gotests -template_dir parameter + gotest_case_exact_match = true, -- exact match test case names comment_placeholder = "  ", - icons = { breakpoint = "🧘", currentpos = "πŸƒ" }, -- set to false to disable - -- this option + icons = { breakpoint = "🧘", currentpos = "πŸƒ" }, -- false to disable + sign_priority = 7, verbose = false, log_path = vim.fn.expand("$HOME") .. "/tmp/gonvim.log", lsp_cfg = false, -- false: do nothing -- true: apply non-default gopls setup defined in go/lsp.lua - -- if lsp_cfg is a table, merge table with with non-default gopls setup in go/lsp.lua, e.g. - lsp_gofumpt = false, -- true: set default gofmt in gopls format to gofumpt - lsp_on_attach = nil, -- nil: do nothing - -- if lsp_on_attach is a function: use this function as on_attach function for gopls, - -- when lsp_cfg is true - lsp_keymaps = true, -- true: apply default lsp keymaps + -- table: merge with non-default gopls setup + lsp_gofumpt = false, -- set default gofmt in gopls to gofumpt + lsp_semantic_highlights = false, -- use highlights from gopls + lsp_on_attach = nil, -- nil: use default on_attach from go/lsp.lua + -- function: use as on_attach for gopls when lsp_cfg is true + lsp_on_client_start = nil, -- called at end of on_attach + lsp_document_formatting = true, -- use gopls to format + lsp_keymaps = true, -- apply default lsp keymaps lsp_codelens = true, - diagnostic = { -- set diagnostic to false to disable vim.diagnostic setup - -- in go.nvim - hdlr = false, -- hook lsp diag handler and send diag to quickfix - underline = true, - -- virtual text setup - virtual_text = { spacing = 0, prefix = 'β– ' }, - signs = true, - update_in_insert = false, - }, + diagnostic = false, -- false: disable diagnostic setup from go.nvim + -- diagnostic = { + -- hdlr = false, + -- underline = true, + -- virtual_text = { spacing = 0, prefix = "β– " }, + -- signs = true, + -- update_in_insert = false, + -- }, lsp_inlay_hints = { enable = true, }, + lsp_diag_update_in_insert = false, + lsp_fmt_async = false, -- async lsp.buf.format + gopls_cmd = nil, -- custom gopls path, e.g. { "/path/to/gopls" } gopls_remote_auto = true, gocoverage_sign = "β–ˆ", - sign_priority = 7, + gocoverage_skip_covered = false, + sign_covered_hl = "String", + sign_partial_hl = "WarningMsg", + sign_uncovered_hl = "Error", + launch_json = nil, -- launch.json path, default: .vscode/launch.json dap_debug = true, - dap_debug_gui = true, - dap_debug_keymap = true, -- true: use keymap for debugger defined in go/dap.lua - -- false: do not use keymap in go/dap.lua. you must define your own. - -- windows: use visual studio style of keymap - dap_vt = true, -- false, true and 'all frames' - textobjects = true, - gopls_cmd = nil, --- you can provide gopls path and cmd if it not in PATH, e.g. cmd = { "/home/ray/.local/nvim/data/lspinstall/go/gopls" } - build_tags = "", --- you can provide extra build tags for tests or debugger - test_runner = "go", -- one of {`go`, `richgo`, `dlv`, `ginkgo`} - run_in_floaterm = false, -- set to true to run in float window. - luasnip = false, -- set true to enable included luasnip - iferr_vertical_shift = 4 -- defines where the cursor will end up vertically from the begining of if err statement after GoIfErr command + dap_debug_gui = {}, -- bool|table for dap-ui setup, false to disable + dap_debug_keymap = true, -- true: use keymaps from go/dap.lua + dap_debug_vt = { enabled_commands = true, all_frames = true }, + dap_port = 38697, -- number or -1 for random port + dap_timeout = 15, + dap_retries = 20, + dap_enrich_config = nil, + build_tags = "", -- extra build tags for tests or debugger + textobjects = true, -- treesitter text objects + test_runner = "go", -- one of {"go", "richgo", "dlv", "ginkgo", "gotestsum"} + run_in_floaterm = false, -- run commands in float window + floaterm = { + posititon = "auto", -- "top", "bottom", "left", "right", "center", "auto" + width = 0.45, + height = 0.98, + title_colors = "nord", + }, + trouble = false, -- use trouble.nvim to open quickfix + test_efm = false, -- errorformat for quickfix + luasnip = false, -- enable included luasnip snippets + username = "", + useremail = "", + disable_per_project_cfg = false, -- disable .gonvim/init.lua loading + iferr_vertical_shift = 4, + iferr_less_highlight = false, -- gray out "if err != nil" statements + on_jobstart = function(cmd) end, + on_stdout = function(err, data) end, + on_stderr = function(err, data) end, + on_exit = function(code, signal, output) end, + ai = { *ai* + enable = false, -- set to true to enable AI features (GoAI, GoCmtAI) + provider = "copilot", -- "copilot" or "openai" + model = nil, -- default: "gpt-4o" (copilot), "gpt-4o-mini" (openai) + api_key_env = "OPENAI_API_KEY", -- env var name that holds the API key + base_url = nil, -- for OpenAI-compatible endpoints + confirm = true, -- confirm before executing translated command + }, } +< vim:tw=78:ts=8:sts=8:sw=8:ft=help:norl:expandtab diff --git a/lua/go/ai.lua b/lua/go/ai.lua new file mode 100644 index 000000000..d897817aa --- /dev/null +++ b/lua/go/ai.lua @@ -0,0 +1,553 @@ +-- LLM-powered natural language command dispatcher for go.nvim +-- Usage: :GoAI run unit test for tags test +-- :GoAI (interactive prompt) + +local M = {} + +local utils = require('go.utils') +local log = utils.log + +-- Cached Copilot API token +local _copilot_token = nil +local _copilot_token_expires = 0 + +-- stylua: ignore +local valid_cmd_set = {} +for _, c in ipairs({ + 'GoTest', 'GoTestFunc', 'GoTestFile', 'GoTestPkg', 'GoTestSubCase', 'GoTestSum', + 'GoAddTest', 'GoAddExpTest', 'GoAddAllTest', 'GoCoverage', + 'GoBuild', 'GoRun', 'GoGenerate', 'GoVet', 'GoLint', 'GoMake', 'GoStop', + 'GoFmt', 'GoImports', + 'GoIfErr', 'GoFillStruct', 'GoFillSwitch', 'GoFixPlurals', 'GoCmt', + 'GoImpl', 'GoEnum', 'GoGenReturn', 'GoJson2Struct', + 'GoAddTag', 'GoRmTag', 'GoClearTag', 'GoModifyTag', + 'GoModTidy', 'GoModVendor', 'GoModDnld', 'GoModGraph', 'GoModWhy', 'GoModInit', + 'GoGet', 'GoWork', + 'GoDoc', 'GoDocBrowser', 'GoAlt', 'GoAltV', 'GoAltS', + 'GoImplements', 'GoPkgOutline', 'GoPkgSymbols', 'GoListImports', 'GoCheat', + 'GoCodeAction', 'GoCodeLenAct', 'GoRename', 'GoGCDetails', + 'GoDebug', 'GoBreakToggle', 'GoBreakSave', 'GoBreakLoad', + 'GoDbgStop', 'GoDbgContinue', 'GoDbgKeys', 'GoCreateLaunch', + 'DapStop', 'DapRerun', 'BreakCondition', 'LogPoint', 'ReplRun', 'ReplToggle', 'ReplOpen', + 'GoInstallBinary', 'GoUpdateBinary', 'GoInstallBinaries', 'GoUpdateBinaries', 'GoTool', + 'GoMockGen', + 'GoEnv', 'GoProject', 'GoToggleInlay', 'GoVulnCheck', + 'GoNew', 'Gomvp', 'Ginkgo', 'GinkgoFunc', 'GinkgoFile', + 'GoGopls', 'GoCmtAI', +}) do + valid_cmd_set[c] = true +end + +local command_catalog = [[ +go.nvim Commands Reference: + +TESTING: +- GoTest [args] β€” Run tests. Args: package path, -v (verbose), -tags=xxx, -bench, -run=pattern, -count=N +- GoTestFunc [args] β€” Run test function under cursor. Args: -v, -tags=xxx +- GoTestFile [args] β€” Run all tests in current file. Args: -v, -tags=xxx +- GoTestPkg [args] β€” Run all tests in current package. Args: -v, -tags=xxx +- GoTestSubCase β€” Run specific table-driven test sub-case under cursor +- GoTestSum [args] β€” Run tests with gotestsum. Args: -w (watch mode) +- GoAddTest β€” Generate test for function under cursor +- GoAddExpTest β€” Generate tests for all exported functions +- GoAddAllTest β€” Generate tests for all functions +- GoCoverage [args] β€” Run tests with coverage display. Args: package path, -t (toggle), -f (file) + +BUILD & RUN: +- GoBuild [args] β€” Build project. Args: package path, e.g. ./... +- GoRun [args] β€” Run the program. Args: any arguments to pass +- GoGenerate [args] β€” Run go generate. Args: package path +- GoVet [args] β€” Run go vet. Args: package path +- GoLint [args] β€” Run golangci-lint. Args: package path +- GoMake β€” Async make +- GoStop [job] β€” Stop a running async job + +FORMAT & IMPORTS: +- GoFmt [formatter] β€” Format file. Args: gofmt, goimports, gofumpt, golines +- GoImports [pkg] β€” Add/remove imports. Args: optional package to import + +CODE GENERATION: +- GoIfErr β€” Generate 'if err != nil' boilerplate at cursor +- GoFillStruct β€” Fill struct literal with default field values +- GoFillSwitch β€” Fill switch statement with all type/enum cases +- GoFixPlurals β€” Merge consecutive same-type function parameters +- GoCmt β€” Generate doc comment for function/struct under cursor +- GoImpl β€” Generate interface stubs. E.g. GoImpl f *File io.Reader +- GoEnum [args] β€” Generate enum helpers +- GoGenReturn β€” Generate return values for function call under cursor +- GoJson2Struct [name] β€” Convert JSON to Go struct. Args: struct name + +STRUCT TAGS: +- GoAddTag [tags] β€” Add struct tags. Args: json, xml, yaml, db, etc. +- GoRmTag [tags] β€” Remove struct tags. Args: tag names +- GoClearTag β€” Remove all struct tags +- GoModifyTag [tag] [transform] β€” Modify tag options. Args: tag name, transform (snakecase, camelcase) + +MODULE MANAGEMENT: +- GoModTidy β€” Run go mod tidy +- GoModVendor β€” Run go mod vendor +- GoModDnld β€” Run go mod download +- GoModGraph β€” Show module dependency graph +- GoModWhy [module] β€” Show why a module is needed +- GoModInit [name] β€” Initialize new Go module +- GoGet [pkg] β€” Run go get. Args: package path +- GoWork [cmd] [path] β€” Go workspace commands. Args: run, use + +NAVIGATION & DOCS: +- GoDoc [symbol] β€” Show documentation. Args: package or symbol name +- GoDocBrowser [symbol] β€” Open docs in browser +- GoAlt β€” Switch between test and implementation file +- GoAltV β€” Switch to alternate file in vertical split +- GoAltS β€” Switch to alternate file in horizontal split +- GoImplements β€” Show interface implementations via LSP +- GoPkgOutline [pkg] β€” Show package outline +- GoPkgSymbols β€” Show package symbols +- GoListImports β€” List imports in current file +- GoCheat [topic] β€” Cheat sheet from cht.sh + +CODE ACTIONS & REFACTORING: +- GoCodeAction β€” Run LSP code actions (visual range supported) +- GoCodeLenAct β€” Run code lens action +- GoRename β€” Rename symbol via LSP +- GoGCDetails β€” Toggle GC optimization details + +DEBUGGING (requires nvim-dap): +- GoDebug [args] β€” Start debugger. Args: -t (test), -r (restart), -n (nearest test), -f (file), -p (package), -s (stop), -b (breakpoint) +- GoBreakToggle β€” Toggle breakpoint at cursor line +- GoBreakSave β€” Save breakpoints to file +- GoBreakLoad β€” Load saved breakpoints +- GoDbgStop β€” Stop debugger +- GoDbgContinue β€” Continue execution in debugger +- GoDbgKeys β€” Show debugger key mappings +- GoCreateLaunch β€” Create .vscode/launch.json +- DapStop β€” Stop DAP session +- DapRerun β€” Rerun last DAP session + +TOOLS & INSTALL: +- GoInstallBinary [tool] β€” Install a Go tool binary +- GoUpdateBinary [tool] β€” Update a Go tool binary +- GoInstallBinaries β€” Install all required tool binaries +- GoUpdateBinaries β€” Update all tool binaries +- GoTool [cmd] β€” Run go tool sub-command + +MOCK: +- GoMockGen [args] β€” Generate mocks. Args: -p (package), -d (destination), -i (interface), -s (source) + +OTHER: +- GoEnv [file] β€” Load environment variables from file +- GoProject β€” Setup project configuration +- GoToggleInlay β€” Toggle LSP inlay hints +- GoVulnCheck β€” Run govulncheck for vulnerability scanning +- GoNew [template] β€” Create project from template +- Gomvp [old] [new] β€” Rename/move packages +- Ginkgo [cmd] β€” Ginkgo framework. Args: generate, bootstrap, build, labels, run, watch +- GinkgoFunc β€” Run Ginkgo test for current function +- GinkgoFile β€” Run Ginkgo tests for current file + +GOPLS LSP COMMANDS (via GoGopls [json_args]): +- GoGopls add_dependency {"GoCmdArgs":["pkg@version"]} β€” Add a module dependency +- GoGopls add_import {"ImportPath":"fmt"} β€” Add an import to the current file +- GoGopls add_test β€” Generate a test for the function at cursor +- GoGopls apply_fix β€” Apply a suggested fix +- GoGopls assembly β€” Show assembly for a function +- GoGopls change_signature β€” Refactor a function signature (remove/reorder params) +- GoGopls check_upgrades β€” Check for module dependency upgrades +- GoGopls diagnose_files β€” Run diagnostics on specified files +- GoGopls doc β€” Open Go documentation for symbol at cursor +- GoGopls edit_go_directive β€” Edit the go directive in go.mod +- GoGopls extract_to_new_file β€” Extract selected code to a new file +- GoGopls free_symbols β€” List free symbols in a selection +- GoGopls gc_details β€” Toggle GC optimization details overlay +- GoGopls generate β€” Run go generate for the current file/package +- GoGopls go_get_package β€” Run go get for a package +- GoGopls list_imports β€” List all imports in the current file +- GoGopls list_known_packages β€” List all known/importable packages +- GoGopls mem_stats β€” Show gopls memory statistics +- GoGopls modify_tags β€” Add/remove/modify struct field tags +- GoGopls modules β€” List modules in the workspace +- GoGopls package_symbols β€” List symbols in a package +- GoGopls packages β€” List packages in the workspace +- GoGopls regenerate_cgo β€” Regenerate cgo definitions +- GoGopls remove_dependency β€” Remove a module dependency +- GoGopls reset_go_mod_diagnostics β€” Reset go.mod diagnostics +- GoGopls run_go_work_command β€” Run a go work command +- GoGopls run_govulncheck β€” Run govulncheck via gopls +- GoGopls run_tests β€” Run tests via gopls +- GoGopls scan_imports β€” Scan for available imports +- GoGopls split_package β€” Split a package into multiple packages +- GoGopls tidy β€” Run go mod tidy +- GoGopls update_go_sum β€” Update go.sum +- GoGopls upgrade_dependency β€” Upgrade a module dependency +- GoGopls vendor β€” Run go mod vendor +- GoGopls vulncheck β€” Run vulnerability check +- GoGopls workspace_stats β€” Show workspace statistics + +AI-POWERED: +- GoCmtAI β€” Generate doc comment for the declaration at cursor using AI +]] + +local system_prompt = [[You are a command translator for go.nvim, a Neovim plugin for Go development. +Your job is to translate natural language requests into the correct go.nvim Vim command. + +Rules: +1. Return ONLY the Vim command to execute. No explanation, no markdown, no backticks, no extra text. +2. The command must start with one of the go.nvim commands from the reference below. +3. Include any necessary arguments exactly as they would be typed in the Vim command line. +4. If the request is ambiguous, choose the most likely command. +5. If the request cannot be mapped to any go.nvim command, return exactly: echo "No matching go.nvim command found" + +]] .. command_catalog + +--- Read Copilot OAuth token from the config files written by copilot.vim / copilot.lua +local function get_copilot_oauth_token() + local paths = { + vim.fn.expand('~/.config/github-copilot/hosts.json'), + vim.fn.expand('~/.config/github-copilot/apps.json'), + } + + for _, path in ipairs(paths) do + local f = io.open(path, 'r') + if f then + local content = f:read('*a') + f:close() + local ok, data = pcall(vim.json.decode, content) + if ok and type(data) == 'table' then + for _, v in pairs(data) do + if type(v) == 'table' and v.oauth_token then + return v.oauth_token + end + end + end + end + end + return nil +end + +--- Parse a curl exit code into a human-readable error message +local function parse_curl_error(exit_code, stderr) + local curl_errors = { + [6] = 'DNS resolution failed - check your network connection', + [7] = 'connection refused - API server may be down', + [28] = 'request timed out - network may be slow or unreachable', + [35] = 'SSL/TLS handshake failed', + [51] = 'SSL certificate verification failed', + [52] = 'server returned empty response', + [56] = 'network data receive error - connection may have been reset', + } + local msg = curl_errors[exit_code] + if msg then + return msg + end + return string.format('curl error %d: %s', exit_code, (stderr or ''):gsub('%s+$', '')) +end + +--- Split curl output (with -w '\n%%{http_code}') into body and status code +local function split_http_response(stdout) + local code = stdout:match('(%d+)%s*$') + local body = code and stdout:sub(1, -(#code + 2)) or stdout + return body, code or '0' +end + +--- Exchange OAuth token for short-lived Copilot API token (cached) +local function get_copilot_api_token(oauth_token, callback) + if _copilot_token and os.time() < _copilot_token_expires then + callback(_copilot_token) + return + end + + vim.system( + { + 'curl', '-s', + '--connect-timeout', '10', + '--max-time', '15', + '-w', '\n%{http_code}', + '-H', 'Authorization: token ' .. oauth_token, + '-H', 'Accept: application/json', + 'https://api.github.com/copilot_internal/v2/token', + }, + { text = true }, + function(result) + vim.schedule(function() + if result.code ~= 0 then + local msg = parse_curl_error(result.code, result.stderr) + vim.notify('go.nvim [AI]: Copilot token request failed: ' .. msg, vim.log.levels.ERROR) + return + end + local stdout = result.stdout or '' + local body, http_code = split_http_response(stdout) + if http_code ~= '200' then + vim.notify('go.nvim [AI]: Copilot token request returned HTTP ' .. http_code .. ': ' .. body:sub(1, 200), vim.log.levels.ERROR) + return + end + local ok, data = pcall(vim.json.decode, body) + if ok and data and data.token then + _copilot_token = data.token + _copilot_token_expires = (data.expires_at or 0) - 60 -- refresh 60s early + callback(data.token) + else + vim.notify('go.nvim [AI]: unexpected Copilot token response', vim.log.levels.ERROR) + end + end) + end + ) +end + +--- Generic helper: POST a chat completion request via curl +local function call_chat_api(url, headers, body, callback) + local cmd = { 'curl', '-s', '--connect-timeout', '10', '--max-time', '30', '-w', '\n%{http_code}', '-X', 'POST' } + for _, h in ipairs(headers) do + table.insert(cmd, '-H') + table.insert(cmd, h) + end + table.insert(cmd, '-d') + table.insert(cmd, '@-') -- read body from stdin + table.insert(cmd, url) + + vim.system(cmd, { text = true, stdin = body }, function(result) + vim.schedule(function() + if result.code ~= 0 then + local msg = parse_curl_error(result.code, result.stderr) + vim.notify('go.nvim [AI]: API request failed: ' .. msg, vim.log.levels.ERROR) + return + end + local stdout = result.stdout or '' + local resp_body, http_code = split_http_response(stdout) + if http_code ~= '200' then + local detail = resp_body:sub(1, 200) + -- Try to extract error message from JSON response + local ok_json, err_data = pcall(vim.json.decode, resp_body) + if ok_json and type(err_data) == 'table' and err_data.error then + local e = err_data.error + detail = type(e) == 'table' and (e.message or vim.inspect(e)) or tostring(e) + end + vim.notify('go.nvim [AI]: HTTP ' .. http_code .. ': ' .. detail, vim.log.levels.ERROR) + return + end + local ok, data = pcall(vim.json.decode, resp_body) + if ok and data and data.choices and data.choices[1] and data.choices[1].message then + callback(vim.trim(data.choices[1].message.content)) + else + vim.notify('go.nvim [AI]: unexpected API response: ' .. resp_body:sub(1, 200), vim.log.levels.ERROR) + end + end) + end) +end + +--- Build the user message with workspace context +local function build_user_message(request) + local file = vim.fn.expand('%:t') or '' + local ft = vim.bo.filetype or '' + return string.format('Current file: %s (filetype: %s)\nRequest: %s', file, ft, request) +end + +--- Build the JSON body for a chat completion +local function build_body(model, sys_prompt, user_msg, opts) + opts = opts or {} + return vim.json.encode({ + model = model, + messages = { + { role = 'system', content = sys_prompt }, + { role = 'user', content = user_msg }, + }, + temperature = opts.temperature or 0, + max_tokens = opts.max_tokens or 200, + }) +end + +--- Validate that the LLM response is a known go.nvim command +local function validate_response(cmd_str) + -- Allow echo for "no match" responses + if cmd_str:match('^echo ') then + return true + end + local cmd_name = cmd_str:match('^:?(%S+)') + return cmd_name and valid_cmd_set[cmd_name] == true +end + +-- Commands that accept a visual range +local range_commands = { + GoCodeAction = true, + GoJson2Struct = true, +} + +--- Process the LLM response: validate, confirm, execute +local function handle_response(cmd_str, confirm, range_prefix) + -- Strip markdown fences if the LLM wrapped the answer + cmd_str = cmd_str:gsub('^```%w*\n?', ''):gsub('\n?```$', '') + cmd_str = vim.trim(cmd_str) + -- take only the first line + cmd_str = cmd_str:match('^([^\n]+)') or cmd_str + + log('go.nvim [AI]: LLM response:', cmd_str) + + if not validate_response(cmd_str) then + log('go.nvim [AI]: unrecognised command:', cmd_str) + vim.notify('go.nvim [AI]: unrecognised command: ' .. cmd_str, vim.log.levels.WARN) + return + end + + -- echo is informational, just show it + if cmd_str:match('^echo ') then + vim.cmd(cmd_str) + return + end + + -- Prepend range prefix for commands that support visual ranges + if range_prefix and range_prefix ~= '' then + local cmd_name = cmd_str:match('^:?(%S+)') + if cmd_name and range_commands[cmd_name] then + cmd_str = range_prefix .. cmd_str + end + end + + log('go.nvim [AI]: executing:', cmd_str) + + if not confirm then + vim.cmd(cmd_str) + return + end + + vim.ui.select({ 'Yes', 'Edit', 'No' }, { + prompt = string.format('Run %s ?', cmd_str), + }, function(choice) + if choice == 'Yes' then + vim.cmd(cmd_str) + elseif choice == 'Edit' then + vim.api.nvim_feedkeys(':' .. cmd_str, 'n', false) + end + end) +end + +--- Send request via GitHub Copilot Chat API (generic) +local function send_copilot_raw(sys_prompt, user_msg, opts, callback) + local oauth = get_copilot_oauth_token() + if not oauth then + vim.notify( + 'go.nvim [AI]: Copilot OAuth token not found. Please install copilot.vim or copilot.lua and run :Copilot auth', + vim.log.levels.ERROR + ) + return + end + + get_copilot_api_token(oauth, function(token) + local cfg = _GO_NVIM_CFG.ai or {} + local model = cfg.model or 'gpt-4o' + local body = build_body(model, sys_prompt, user_msg, opts) + local nvim_ver = string.format('%s.%s.%s', vim.version().major, vim.version().minor, vim.version().patch) + local headers = { + 'Content-Type: application/json', + 'Authorization: Bearer ' .. token, + 'Copilot-Integration-Id: vscode-chat', + 'Editor-Version: Neovim/' .. nvim_ver, + 'Editor-Plugin-Version: go.nvim/1.0.0', + 'User-Agent: go.nvim/1.0.0', + } + call_chat_api('https://api.githubcopilot.com/chat/completions', headers, body, callback) + end) +end + +--- Send request via OpenAI-compatible API (generic) +local function send_openai_raw(sys_prompt, user_msg, opts, callback) + local cfg = _GO_NVIM_CFG.ai or {} + local env_name = cfg.api_key_env or 'OPENAI_API_KEY' + local api_key = os.getenv(env_name) + local base_url = cfg.base_url or 'https://api.openai.com/v1' + local model = cfg.model or 'gpt-4o-mini' + + if not api_key or api_key == '' then + vim.notify( + 'go.nvim [AI]: API key not found. Set the ' .. env_name .. ' environment variable', + vim.log.levels.ERROR + ) + return + end + + local body = build_body(model, sys_prompt, user_msg, opts) + local headers = { + 'Content-Type: application/json', + 'Authorization: Bearer ' .. api_key, + } + call_chat_api(base_url .. '/chat/completions', headers, body, callback) +end + +--- Entry point: :GoAI run unit test for tags test +function M.run(opts) + local cfg = _GO_NVIM_CFG.ai or {} + if not cfg.enable then + vim.notify('go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoAI', vim.log.levels.WARN) + return + end + + local prompt + local range_prefix = '' + + if type(opts) == 'table' then + prompt = table.concat(opts.fargs or {}, ' ') + -- Capture visual range if the command was called with one + if opts.range and opts.range == 2 then + range_prefix = string.format('%d,%d', opts.line1, opts.line2) + end + else + -- Legacy: called with varargs + prompt = opts or '' + end + + if prompt == '' then + vim.ui.input({ prompt = 'go.nvim AI> ' }, function(input) + if input and input ~= '' then + M._dispatch(input, range_prefix) + end + end) + return + end + + M._dispatch(prompt, range_prefix) +end + +--- Dispatch the natural language request to the configured LLM provider +function M._dispatch(prompt, range_prefix) + local cfg = _GO_NVIM_CFG.ai or {} + local provider = cfg.provider or 'copilot' + local confirm = cfg.confirm ~= false -- default true + + vim.notify('go.nvim [AI]: thinking …', vim.log.levels.INFO) + + local user_msg = build_user_message(prompt) + + local function on_resp(resp) + handle_response(resp, confirm, range_prefix) + end + + if provider == 'copilot' then + send_copilot_raw(system_prompt, user_msg, {}, on_resp) + elseif provider == 'openai' then + send_openai_raw(system_prompt, user_msg, {}, on_resp) + else + vim.notify('go.nvim [AI]: unknown provider "' .. provider .. '"', vim.log.levels.ERROR) + end +end + +--- Generic LLM request for use by other modules. +--- @param sys_prompt string The system prompt +--- @param user_msg string The user message +--- @param opts table|nil Optional: { temperature, max_tokens } +--- @param callback function Called with the response text string +function M.request(sys_prompt, user_msg, opts, callback) + opts = opts or {} + local cfg = _GO_NVIM_CFG.ai or {} + if not cfg.enable then + vim.notify('go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup', vim.log.levels.WARN) + return + end + local provider = cfg.provider or 'copilot' + + if provider == 'copilot' then + send_copilot_raw(sys_prompt, user_msg, opts, callback) + elseif provider == 'openai' then + send_openai_raw(sys_prompt, user_msg, opts, callback) + else + vim.notify('go.nvim [AI]: unknown provider "' .. provider .. '"', vim.log.levels.ERROR) + end +end + +return M diff --git a/lua/go/commands.lua b/lua/go/commands.lua index 4d23d2211..4b92498be 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -470,6 +470,9 @@ return { create_cmd('GoCmt', function(_) require('go.comment').gen() end) + create_cmd('GoCmtAI', function(_) + require('go.comment').gen_ai() + end) create_cmd('GoRename', function(_) require('go.rename').lsprename() end) @@ -612,5 +615,44 @@ return { end, { nargs = '*', }) + + create_cmd('GoGopls', function(opts) + local gopls = require('go.gopls') + local subcmd = opts.fargs[1] + if not subcmd then + vim.notify('Usage: GoGopls [json_args]', vim.log.levels.WARN) + return + end + if not gopls.cmds[subcmd] then + vim.notify('Unknown gopls subcommand: ' .. subcmd, vim.log.levels.WARN) + return + end + local arg = {} + if opts.fargs[2] then + local json_str = table.concat(opts.fargs, ' ', 2) + local ok, parsed = pcall(vim.json.decode, json_str) + if ok then + arg = parsed + else + -- treat remaining args as key=value pairs + for i = 2, #opts.fargs do + local k, v = opts.fargs[i]:match('^(.-)=(.+)$') + if k then + arg[k] = v + end + end + end + end + gopls.cmds[subcmd](arg) + end, { + complete = function(_, _, _) + return vim.tbl_keys(require('go.gopls').cmds) + end, + nargs = '+', + }) + + create_cmd('GoAI', function(opts) + require('go.ai').run(opts) + end, { nargs = '*', range = true }) end, } diff --git a/lua/go/comment.lua b/lua/go/comment.lua index 36f93f184..aa801b971 100644 --- a/lua/go/comment.lua +++ b/lua/go/comment.lua @@ -85,4 +85,84 @@ comment.gen = function() return c end +local comment_system_prompt = [[You are a Go documentation expert. +Generate a Go doc comment for the given code declaration. + +Rules: +1. Follow Go documentation conventions (https://go.dev/doc/comment). +2. The comment must start with "// " where is the identifier name. +3. For package clauses, start with "// Package ". +4. Be concise but informative. Describe what it does, not how. +5. If the code has parameters, mention important ones only when their purpose is non-obvious. +6. Return ONLY the comment lines (each starting with "//"). No code, no markdown fences, no extra text. +7. If it's a multi-line comment, use multiple "// " lines. +8. Do not add a blank line between comment lines. +]] + +--- Get the declaration node and its source text at cursor +local function get_declaration_at_cursor() + local bufnr = api.nvim_get_current_buf() + + -- Try each node type in order + local getters = { + { fn = require('go.ts.go').get_package_node_at_pos, kind = 'package' }, + { fn = require('go.ts.go').get_func_method_node_at_pos, kind = 'function' }, + { fn = require('go.ts.go').get_struct_node_at_pos, kind = 'struct' }, + { fn = require('go.ts.go').get_interface_node_at_pos, kind = 'interface' }, + { fn = require('go.ts.go').get_type_node_at_pos, kind = 'type' }, + } + + for _, g in ipairs(getters) do + local ns = g.fn() + if ns ~= nil and ns ~= {} and ns.declaring_node then + local source = vim.treesitter.get_node_text(ns.declaring_node, bufnr) + return ns, source, g.kind + end + end + return nil, nil, nil +end + +--- Generate doc comment using AI/Copilot +comment.gen_ai = function() + local ns, source, kind = get_declaration_at_cursor() + if not ns or not source then + vim.notify('go.nvim [AI Comment]: no Go declaration found at cursor', vim.log.levels.WARN) + return + end + + local file = vim.fn.expand('%:t') or '' + local user_msg = string.format('File: %s\nKind: %s\n\n```go\n%s\n```', file, kind, source) + + vim.notify('go.nvim [AI Comment]: generating …', vim.log.levels.INFO) + + require('go.ai').request(comment_system_prompt, user_msg, { max_tokens = 300 }, function(resp) + -- Strip markdown fences if present + resp = resp:gsub('^```%w*\n?', ''):gsub('\n?```$', '') + resp = vim.trim(resp) + + -- Split into lines and validate each starts with "//" + local lines = vim.split(resp, '\n', { plain = true }) + local comment_lines = {} + for _, line in ipairs(lines) do + line = vim.trim(line) + if line:match('^//') then + table.insert(comment_lines, line) + end + end + + if #comment_lines == 0 then + vim.notify('go.nvim [AI Comment]: LLM returned no valid comment lines', vim.log.levels.WARN) + return + end + + local row = ns.dim.s.r + -- Insert comment lines above the declaration + for i = #comment_lines, 1, -1 do + vim.fn.append(row - 1, comment_lines[i]) + end + -- Position cursor at the end of the first comment line + vim.fn.cursor(row, #comment_lines[1] + 1) + end) +end + return comment diff --git a/lua/go/gopls.lua b/lua/go/gopls.lua index d6c7ddaa1..92aef656e 100644 --- a/lua/go/gopls.lua +++ b/lua/go/gopls.lua @@ -7,6 +7,56 @@ local cmds = {} local has_nvim0_10 = vim.fn.has('nvim-0.10') == 1 local has_nvim0_11 = vim.fn.has('nvim-0.11') == 1 -- https://go.googlesource.com/tools/+/refs/heads/master/gopls/doc/commands.md +-- https://github.com/golang/tools/blob/master/gopls/internal/doc/api.json +-- https://github.com/golang/tools/blob/master/gopls/internal/protocol/command/command_gen.go +--[[ + AddDependency Command = "gopls.add_dependency" + AddImport Command = "gopls.add_import" + AddTelemetryCounters Command = "gopls.add_telemetry_counters" + AddTest Command = "gopls.add_test" + ApplyFix Command = "gopls.apply_fix" + Assembly Command = "gopls.assembly" + ChangeSignature Command = "gopls.change_signature" + CheckUpgrades Command = "gopls.check_upgrades" + ClientOpenURL Command = "gopls.client_open_url" + DiagnoseFiles Command = "gopls.diagnose_files" + Doc Command = "gopls.doc" + EditGoDirective Command = "gopls.edit_go_directive" + ExtractToNewFile Command = "gopls.extract_to_new_file" + FetchVulncheckResult Command = "gopls.fetch_vulncheck_result" + FreeSymbols Command = "gopls.free_symbols" + GCDetails Command = "gopls.gc_details" + Generate Command = "gopls.generate" + GoGetPackage Command = "gopls.go_get_package" + ListImports Command = "gopls.list_imports" + ListKnownPackages Command = "gopls.list_known_packages" + LSP Command = "gopls.lsp" + MaybePromptForTelemetry Command = "gopls.maybe_prompt_for_telemetry" + MemStats Command = "gopls.mem_stats" + ModifyTags Command = "gopls.modify_tags" + Modules Command = "gopls.modules" + MoveType Command = "gopls.move_type" + PackageSymbols Command = "gopls.package_symbols" + Packages Command = "gopls.packages" + RegenerateCgo Command = "gopls.regenerate_cgo" + RemoveDependency Command = "gopls.remove_dependency" + ResetGoModDiagnostics Command = "gopls.reset_go_mod_diagnostics" + RunGoWorkCommand Command = "gopls.run_go_work_command" + RunGovulncheck Command = "gopls.run_govulncheck" + RunTests Command = "gopls.run_tests" + ScanImports Command = "gopls.scan_imports" + SplitPackage Command = "gopls.split_package" + StartDebugging Command = "gopls.start_debugging" + StartProfile Command = "gopls.start_profile" + StopProfile Command = "gopls.stop_profile" + Tidy Command = "gopls.tidy" + UpdateGoSum Command = "gopls.update_go_sum" + UpgradeDependency Command = "gopls.upgrade_dependency" + Vendor Command = "gopls.vendor" + Views Command = "gopls.views" + Vulncheck Command = "gopls.vulncheck" + WorkspaceStats Command = "gopls.workspace_stats" +]] -- local gopls_cmds = { 'gopls.add_dependency', @@ -68,6 +118,7 @@ local gopls_with_edit = { 'gopls.check_upgrades', 'gopls.change_signature', } +--- check_for_error inspects LSP response for error entries and notifies the user. local function check_for_error(msg) if msg ~= nil and type(msg[1]) == 'table' then for k, v in pairs(msg[1]) do @@ -80,6 +131,8 @@ local function check_for_error(msg) end end +--- apply_changes executes a gopls workspace command that produces document edits +--- and applies them to the current buffer via vim.lsp.util.apply_workspace_edit. local function apply_changes(cmd, args) local bufnr = vim.api.nvim_get_current_buf() local clients = vim.lsp.get_clients({ bufnr = bufnr }) @@ -193,12 +246,15 @@ for _, gopls_cmd in ipairs(gopls_cmds) do end M.cmds = cmds +--- import adds a Go import path via the gopls.add_import command and reformats the file. M.import = function(path) cmds.add_import({ ImportPath = path, }, require('go.format').gofmt) end +--- change_signature invokes gopls.change_signature to remove a parameter +--- identified by the current visual selection or cursor position. M.change_signature = function() local gopls = vim.lsp.get_clients({ bufnr = 0, name = 'gopls' }) if not gopls then @@ -222,6 +278,7 @@ M.change_signature = function() cmds.change_signature(lsp_params) end +--- gc_details toggles the display of GC optimization details for the given file URI. M.gc_details = function(args) local uri = vim.uri_from_bufnr(0) if args ~= nil then @@ -234,6 +291,8 @@ M.gc_details = function(args) cmds.gc_details(lsp_params) end +--- list_imports returns the imports of the file at path (default: current file) +--- as a table keyed by import group with string entries like "name:path" or "path". M.list_imports = function(path) path = path or vim.fn.expand('%:p') local resp = cmds.list_imports({ @@ -257,6 +316,7 @@ M.list_imports = function(path) return result, resp end +--- list_pkgs returns the list of known packages from gopls for auto-completion. M.list_pkgs = function() local resp = cmds.list_known_packages() or {} @@ -270,15 +330,19 @@ M.list_pkgs = function() return pkgs end +--- package_symbols retrieves and renders the symbols for a given package. M.package_symbols = function(pkg, render) -- not sure how to add pkg info, leave it empty for now cmds.package_symbols({}, render) end +--- tidy runs the gopls.tidy command to tidy go.mod. M.tidy = function(args) cmds.tidy(args) end +--- doc opens the Go documentation for the symbol at cursor (or the given URI) +--- in the system's default browser. M.doc = function(args) local gopls = vim.lsp.get_clients({ bufnr = 0, name = 'gopls' }) if not gopls then @@ -319,6 +383,8 @@ M.doc = function(args) end -- check_for_upgrades({Modules = {'package'}}) +--- version returns the installed gopls version string (e.g. "0.16.1"). +--- The result is cached in stdpath('cache')/version.txt. function M.version() local cache_dir = vfn.stdpath('cache') local path = string.format('%s%sversion.txt', cache_dir, utils.sep()) @@ -367,6 +433,7 @@ function M.version() return version end +--- get_current_gomod reads the module name from go.mod in the current directory. local get_current_gomod = function() local file = io.open('go.mod', 'r') if file == nil then @@ -383,6 +450,7 @@ local get_current_gomod = function() return mod_name end +--- get_build_flags returns the build tags/flags configured for the current project. local function get_build_flags() local get_build_tags = require('go.gotest').get_build_tags local tags = get_build_tags() @@ -432,6 +500,8 @@ M.semanticTokenModifiers = { struct = true, } +--- setups returns the default gopls LSP client configuration table, +--- including capabilities, filetypes, settings, codelenses, and inlay hints. M.setups = function() local diag_cfg = vim.diagnostic.config() or {} local update_in_insert = diag_cfg.update_in_insert or false diff --git a/lua/go/gotest.lua b/lua/go/gotest.lua index 756068c49..1974519e6 100644 --- a/lua/go/gotest.lua +++ b/lua/go/gotest.lua @@ -359,7 +359,7 @@ M.test = function(...) run_test(fpath, args) end -M.test_suit = function(...) +M.test_suite = function(...) local args = { ... } log(args) From 8f2fd6bb785cbd6af88e469279eca29631ee3a5a Mon Sep 17 00:00:00 2001 From: ray-x Date: Mon, 2 Mar 2026 15:40:52 +1100 Subject: [PATCH 03/21] update health --- lua/go/health.lua | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lua/go/health.lua b/lua/go/health.lua index 634658e14..97d84162a 100644 --- a/lua/go/health.lua +++ b/lua/go/health.lua @@ -256,6 +256,46 @@ local function lsp_check() end end +local function ai_check() + start('AI (GoAI / GoCmtAI)') + local cfg = _GO_NVIM_CFG.ai or {} + if not cfg.enable then + info('AI is disabled (ai.enable = false)') + return + end + ok('AI is enabled') + + local provider = cfg.provider or 'copilot' + info('Provider: ' .. provider) + + if provider == 'copilot' then + local paths = { + vim.fn.expand('~/.config/github-copilot/hosts.json'), + vim.fn.expand('~/.config/github-copilot/apps.json'), + } + local found = false + for _, path in ipairs(paths) do + if vfn.filereadable(path) == 1 then + found = true + ok('Copilot token file found: ' .. path) + break + end + end + if not found then + error('Copilot token file not found. Run :Copilot auth to authenticate') + end + elseif provider == 'openai' then + local env_name = cfg.api_key_env or 'OPENAI_API_KEY' + if os.getenv(env_name) then + ok('$' .. env_name .. ' is set') + else + error('$' .. env_name .. ' is not set') + end + else + warn('Unknown AI provider: ' .. provider) + end +end + function M.check() if vim.fn.has('nvim-0.10') == 0 then warn('Suggested neovim version 0.10 or higher') @@ -265,6 +305,7 @@ function M.check() lsp_check() plugin_check() env_check() + ai_check() end return M From 557f5a394e070669e955681c2db13ea76227ec71 Mon Sep 17 00:00:00 2001 From: ray-x Date: Thu, 5 Mar 2026 10:23:33 +1100 Subject: [PATCH 04/21] add GoCodeReview command --- lua/go/ai.lua | 324 +++++++++++++++++++++++++++++++++++++++++++- lua/go/commands.lua | 10 ++ 2 files changed, 332 insertions(+), 2 deletions(-) diff --git a/lua/go/ai.lua b/lua/go/ai.lua index d897817aa..5156e5460 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -33,7 +33,7 @@ for _, c in ipairs({ 'GoMockGen', 'GoEnv', 'GoProject', 'GoToggleInlay', 'GoVulnCheck', 'GoNew', 'Gomvp', 'Ginkgo', 'GinkgoFunc', 'GinkgoFile', - 'GoGopls', 'GoCmtAI', + 'GoGopls', 'GoCmtAI', 'GoCodeReview', }) do valid_cmd_set[c] = true end @@ -184,6 +184,7 @@ GOPLS LSP COMMANDS (via GoGopls [json_args]): AI-POWERED: - GoCmtAI β€” Generate doc comment for the declaration at cursor using AI +- GoCodeReview β€” Review the current Go file (or visual selection) with AI; outputs findings to the vim quickfix list ]] local system_prompt = [[You are a command translator for go.nvim, a Neovim plugin for Go development. @@ -527,7 +528,326 @@ function M._dispatch(prompt, range_prefix) end end ---- Generic LLM request for use by other modules. +-- ─── GoCodeReview ──────────────────────────────────────────────────────────── + +--- Detect the default branch of the current git repo (main or master). +--- Falls back to 'main' if neither is found. +--- @return string +local function detect_default_branch() + -- Check remote HEAD first (most reliable) + local h = vim.fn.systemlist('git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null') + if vim.v.shell_error == 0 and h[1] then + local branch = h[1]:match('refs/remotes/origin/(.+)') + if branch then + return branch + end + end + -- Fallback: check if main or master exists locally + for _, name in ipairs({ 'main', 'master' }) do + vim.fn.system('git rev-parse --verify ' .. name .. ' 2>/dev/null') + if vim.v.shell_error == 0 then + return name + end + end + return 'main' +end + +--- Get the unified diff of a file against a branch. +--- @param filepath string Absolute path to the file +--- @param branch string Branch name to diff against +--- @param callback function Called with (diff_text, err_msg) +local function get_git_diff(filepath, branch, callback) + local rel = vim.fn.fnamemodify(filepath, ':.') + vim.system( + { 'git', 'diff', branch .. '...HEAD', '--', rel }, + { text = true }, + function(result) + vim.schedule(function() + if result.code ~= 0 then + callback(nil, 'git diff failed: ' .. (result.stderr or ''):gsub('%s+$', '')) + return + end + local diff = vim.trim(result.stdout or '') + if diff == '' then + callback(nil, 'no changes against ' .. branch) + return + end + callback(diff, nil) + end) + end + ) +end + +local code_review_system_prompt = [[You are an experienced Golang code reviewer. Your task is to review Go language source code for correctness, readability, performance, best practices, and style. Carefully analyze the given Go code snippet or file and provide specific actionable feedback to improve quality. Identify issues such as bugs, inefficient constructs, poor naming, inconsistent formatting, concurrency pitfalls, error handling mistakes, or deviations from idiomatic Go. Suggest precise code changes and explain why they improve the code. + +When reviewing, reason step-by-step about each aspect of the code before concluding. Be polite, professional, and constructive. + +# Audit Categories + +1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. +2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. +3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. +4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. +5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. +6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %w. +7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. +8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. +9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. +10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. + +# Instructions + +1. Read the entire Go code provided. +2. For each audit category above, check whether any issues apply. +3. Assess functionality and correctness. +4. Evaluate code readability and style against Go conventions. +5. Check for performance or concurrency issues. +6. Review error handling and package usage. +7. Provide only actionable improvements β€” skip praise or explanations of what is already good. + +# Output Format + +The source code is provided with explicit line markers in the format "L|" at the start of each line. +For example: + L10| func main() { + L11| os.Getenv("KEY") + L12| } +If you find an issue on the line starting with "L11|", you MUST output line number 11. + +If there are NO improvements needed: +- Output exactly one line: a brief overall summary (e.g. "Code looks idiomatic and correct."). + +If there ARE improvements, output ONLY lines in vim quickfix format: + ::: : +where is: + error β€” compile errors and logic errors only (code will not build or produces wrong results) + warning β€” issues that must be handled for production: memory leaks, heap escapes, missing/incorrect timeouts, unclosed resources, unhandled signals, etc. + suggestion β€” all other improvements: style, naming, readability, idiomatic Go, minor refactors, etc. + +Example input: + L41| params := map[string]string{ + L42| "ProjectID": projectID, + L43| "ServingConfig": os.Getenv("GCP_SEARCH_SERVING_CONFIG"), + L44| } + +Example output (issue is on L43 where os.Getenv is called): + main.go:43:20: warning: os.Getenv called inline; consider reading env var at startup and validating it + +CRITICAL: Read the "L|" prefix of the EXACT line containing the issue. That number is the line number you must use. Do NOT use the line number of a nearby or enclosing line. + +Rules: +- Do NOT output any introduction, summary header, markdown, or conclusion. +- Do NOT use code blocks or bullet points. +- Each issue must be a separate line in the exact quickfix format above. +- Line numbers MUST match the prefixed line numbers in the provided code. If exact line is unknown, use line 1. +- Focus on practical, specific improvements only. + +If code is not provided, output exactly: error: no Go source code provided for review. +]] + +local diff_review_system_prompt = [[You are an experienced Golang code reviewer. You are reviewing a unified diff (git diff) of Go source code changes against a base branch. Focus ONLY on the changed lines (lines starting with + or context around them). Evaluate the changes for correctness, readability, performance, best practices, and style. + +IMPORTANT: Use the NEW file line numbers from the diff hunk headers (the second number in @@ -a,b +c,d @@). For added/changed lines (starting with +), compute the actual file line number by counting from the hunk start. + +# Audit Categories + +1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. +2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. +3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. +4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. +5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. +6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %w. +7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. +8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. +9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. +10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. + +# Instructions + +1. Read the unified diff carefully. +2. Focus only on the added/modified code (+ lines). +3. For each audit category above, check whether any issues apply to the changed code. +4. Evaluate changed code for bugs, style, performance, concurrency, error handling. +5. Skip praise β€” output improvements only. + +# Output Format + +If there are NO improvements needed: +- Output exactly one line: a brief summary (e.g. "Changes look correct and idiomatic."). + +If there ARE improvements, output ONLY lines in vim quickfix format: + ::: : +where is: + error β€” compile errors and logic errors only (code will not build or produces wrong results) + warning β€” issues that must be handled for production: memory leaks, heap escapes, missing/incorrect timeouts, unclosed resources, unhandled signals, etc. + suggestion β€” all other improvements: style, naming, readability, idiomatic Go, minor refactors, etc. + +Rules: +- Do NOT output any introduction, summary header, markdown, or conclusion. +- Do NOT use code blocks or bullet points. +- Each issue must be a separate line in the exact quickfix format above. +- Line numbers must be the NEW file line numbers (post-change), 1-based. +- Focus on practical, specific improvements only. +]] + +--- Parse quickfix lines from the LLM response and open the quickfix list. +--- Lines not matching the format are silently skipped. +--- @param response string Raw LLM output +--- @param filename string Absolute path to the reviewed file +local function handle_review_response(response, filename) + response = vim.trim(response) + + -- Strip accidental markdown fences + response = response:gsub('^```%w*\n?', ''):gsub('\n?```$', '') + response = vim.trim(response) + + local lines = vim.split(response, '\n', { plain = true }) + + -- Detect "no issues" case: single non-quickfix line + if #lines == 1 and not lines[1]:match('^[^:]+:%d+:') then + vim.notify('go.nvim [CodeReview]: ' .. lines[1], vim.log.levels.INFO) + return + end + + local qflist = {} + for _, line in ipairs(lines) do + line = vim.trim(line) + if line ~= '' then + -- Try to parse: filename:lnum:col: type: text + -- or filename:lnum: type: text (col optional) + local fname, lnum, col, text = line:match('^([^:]+):(%d+):(%d+):%s*(.+)$') + if not fname then + fname, lnum, text = line:match('^([^:]+):(%d+):%s*(.+)$') + col = 1 + end + if fname and lnum and text then + -- Derive type (E/W/I) from leading severity word + local type_char = 'W' + local severity = text:match('^(%a+):') + if severity then + local sl = severity:lower() + if sl == 'error' then + type_char = 'E' + elseif sl == 'suggestion' or sl == 'info' or sl == 'note' then + type_char = 'I' + end + end + table.insert(qflist, { + filename = filename, + lnum = tonumber(lnum) or 1, + col = tonumber(col) or 1, + text = text, + type = type_char, + }) + else + -- Fallback: treat unrecognised line as a general warning at line 1 + if line ~= '' then + table.insert(qflist, { + filename = filename, + lnum = 1, + col = 1, + text = line, + type = 'W', + }) + end + end + end + end + + if #qflist == 0 then + vim.notify('go.nvim [CodeReview]: great job! No issues found.', vim.log.levels.INFO) + return + end + + vim.fn.setqflist({}, 'r', { title = 'GoCodeReview', items = qflist }) + vim.cmd('copen') + vim.notify(string.format('go.nvim [CodeReview]: %d issue(s) added to quickfix', #qflist), vim.log.levels.INFO) +end + +--- Entry point for :GoCodeReview [-d [branch]] +--- Reviews the current buffer, visual selection, or diff against a branch. +--- :GoCodeReview β€” review entire file +--- :'<,'>GoCodeReview β€” review visual selection +--- :GoCodeReview -d β€” review only changes vs main/master (auto-detected) +--- :GoCodeReview -d develop β€” review only changes vs 'develop' +--- @param opts table Standard nvim command opts (range, line1, line2, fargs) +function M.code_review(opts) + local cfg = _GO_NVIM_CFG.ai or {} + if not cfg.enable then + vim.notify('go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoCodeReview', vim.log.levels.WARN) + return + end + + local fargs = (type(opts) == 'table' and opts.fargs) or {} + + -- Parse -d [branch] flag + local diff_mode = false + local diff_branch = nil + for i, arg in ipairs(fargs) do + if arg == '-d' or arg == '--diff' then + diff_mode = true + if fargs[i + 1] and not fargs[i + 1]:match('^%-') then + diff_branch = fargs[i + 1] + end + break + end + end + + local filename = vim.fn.expand('%:p') + + if diff_mode then + local branch = diff_branch or detect_default_branch() + vim.notify('go.nvim [CodeReview]: diffing against ' .. branch .. ' …', vim.log.levels.INFO) + get_git_diff(filename, branch, function(diff, err) + if err then + vim.notify('go.nvim [CodeReview]: ' .. err, vim.log.levels.WARN) + return + end + local short_name = vim.fn.expand('%:t') + local user_msg = string.format( + 'File: %s\nBase branch: %s\n\n```diff\n%s\n```', + short_name, branch, diff + ) + M.request(diff_review_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) + handle_review_response(resp, filename) + end) + end) + return + end + + -- Full-file / visual-selection review + local lines + local start_line = 1 + if type(opts) == 'table' and opts.range and opts.range == 2 then + lines = vim.api.nvim_buf_get_lines(0, opts.line1 - 1, opts.line2, false) + start_line = opts.line1 + else + lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + end + + if #lines == 0 then + vim.notify('go.nvim [CodeReview]: buffer is empty', vim.log.levels.WARN) + return + end + + -- Prefix each line with its file line number so the LLM references exact positions + local numbered = {} + for i, line in ipairs(lines) do + table.insert(numbered, string.format('L%d| %s', start_line + i - 1, line)) + end + local code = table.concat(numbered, '\n') + local short_name = vim.fn.expand('%:t') + local user_msg = string.format('File: %s\n\n```go\n%s\n```', short_name, code) + + vim.notify('go.nvim [CodeReview]: reviewing …', vim.log.levels.INFO) + + M.request(code_review_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) + handle_review_response(resp, filename) + end) +end + +-- ─── Public request helper ──────────────────────────────────────────────────── + --- @param sys_prompt string The system prompt --- @param user_msg string The user message --- @param opts table|nil Optional: { temperature, max_tokens } diff --git a/lua/go/commands.lua b/lua/go/commands.lua index 4b92498be..eac33b7ce 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -654,5 +654,15 @@ return { create_cmd('GoAI', function(opts) require('go.ai').run(opts) end, { nargs = '*', range = true }) + + create_cmd('GoCodeReview', function(opts) + require('go.ai').code_review(opts) + end, { + nargs = '*', + range = true, + complete = function(_, _, _) + return { '-d', '--diff' } + end, + }) end, } From 5f1bedcfa8d14fec6ad18ce38e93e47b8fd70df5 Mon Sep 17 00:00:00 2001 From: ray-x Date: Thu, 5 Mar 2026 11:22:06 +1100 Subject: [PATCH 05/21] update README, update GoDocAI --- README.md | 156 +++++++++++++++++++++++++---------- doc/go.txt | 40 ++++++++- lua/go.lua | 8 ++ lua/go/ai.lua | 3 +- lua/go/commands.lua | 9 ++ lua/go/godoc.lua | 196 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 366 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 76cd89ef1..aa7db0254 100644 --- a/README.md +++ b/README.md @@ -5,46 +5,66 @@ possible. PR & Suggestions are welcome. The plugin covers most features required for a gopher. -- Perproject setup. Allows you setup plugin behavior per project based on project files(launch.json, .gonvim) -- Async jobs with libuv -- Syntax highlight & Texobject: Native treesitter support is faster and more accurate. All you need is a theme support - treesitter, try [aurora](https://github.com/ray-x/aurora), [starry.nvim](https://github.com/ray-x/starry.nvim). Also, - there are quite a few listed in [awesome-neovim](https://github.com/rockerBOO/awesome-neovim) -- All the GoToXxx (E.g reference, implementation, definition, goto doc, peek code/doc etc) You need lspconfig setup. - There are lots of posts on how to set it up. You can also check my [navigator](https://github.com/ray-x/navigator.lua) - gopls setup [lspconfig.lua](https://github.com/ray-x/navigator.lua/blob/master/lua/navigator/lspclient/clients.lua) -- Show interface implementation with virtual_text -- gopls commands: e.g. fillstruct, organize imports, list modules, list packages, gc_details, generate, change - signature, etc. -- Runtime lint/vet/compile: Supported by LSP (once you set up your LSP client), GoLint with golangci-lint(v2) also - supported -- Build/Make/Test: Go.nvim provides support for these by an async job wrapper. -- Test coverage: run test coverage and show coverage sign and function metrics -- Dlv Debug: with [nvim-dap](https://github.com/mfussenegger/nvim-dap) and - [Dap UI](https://github.com/rcarriga/nvim-dap-ui). Go adapter included, zero config for your debug setup. -- Load vscode launch configuration -- Unit test: generate unit test framework with [gotests](https://github.com/cweill/gotests). Run test with - ginkgo/gotestsum/go test -- Add and remove tag for struct with tag modify(gomodifytags) -- Code format: Supports LSP format and GoFmt(with golines) -- CodeLens : gopls codelens and codelens action support -- Comments: Add autodocument for your package/function/struct/interface. This feature is unique and can help you - suppress golint errors... -- Go to alternative go file (between test and source) -- Test with ginkgo, gotestsum inside floaterm (to enable floaterm, guihua.lua has to be installed) -- Code refactor made easy: GoFixPlural, FixStruct, FixSwitch, Add comment, IfErr, ModTidy, GoGet, extract function/block - with codeactions... Most of the tools are built on top of treesitter AST or go AST. Fast and accurate. -- GoCheat get go cheatsheet from [cheat.sh](https://cheat.sh/). -- Smart build tag detection when debug/run tests (e.g. `//go:build integration`) -- Generate mocks with mockgen -- Inlay hints: gopls (version 0.9.x or greater) inlay hints; version 0.10.x inlay hints are enabled by default. -- luasnip: go.nvim included a feature rich luasnips you definitally need to try. -- Treesitter highlight injection: go.nvim included a treesitter highlight injection for SQL and JSON. -- Treesitter also injects highlight for `go template`, `gohtmltmpl` -- Generate return value for current function -- Generate go file with template -- Generate go struct from json -- MockGen support +**LSP & Navigation** +- gopls commands: fillstruct, organize imports, list modules/packages, gc_details, generate, change signature, etc. +- All GoToXxx (reference, implementation, definition, doc, peek code/doc, etc.) via gopls/lspconfig. + Check [navigator.lua](https://github.com/ray-x/navigator.lua) for a floating UI experience +- Show interface implementations with virtual text +- Inlay hints (gopls 0.9+; enabled by default in 0.10+) +- CodeLens & CodeAction support + +**Build, Test & Coverage** +- Async build/make/test with libuv job wrapper +- Test with `go test`, [gotestsum](https://github.com/gotestyourself/gotestsum), or [ginkgo](https://github.com/onsi/ginkgo) β€” including floaterm support +- Generate unit tests with [gotests](https://github.com/cweill/gotests) (table-driven, testify) +- Test coverage: run coverage, display signs, and show function metrics +- Smart build-tag detection for debug/test runs (e.g. `//go:build integration`) + +**Code Generation & Refactoring** +- `GoIfErr`, `GoFillStruct`, `GoFillSwitch`, `GoFixPlurals`, `GoGenReturn` β€” powered by treesitter/go AST +- `GoImpl` β€” generate interface method stubs +- `GoEnum` β€” generate enum helpers +- `GoJson2Struct` β€” convert JSON/YAML to Go structs +- `GoMockGen` β€” generate mocks with mockgen +- `GoNew` β€” create files/projects from templates (including `gonew`) +- Struct tag management with [gomodifytags](https://github.com/fatih/gomodifytags) + +**Formatting & Linting** +- Format via LSP (gopls) or CLI (`gofumpt`, `goimports`, `golines`) +- Lint with golangci-lint (v2) β€” LSP diagnostics or async background checks + +**Debugging** +- Dlv debug with [nvim-dap](https://github.com/mfussenegger/nvim-dap) and [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui) β€” zero-config Go adapter included +- Load VSCode `launch.json` configurations + +**AI-Powered** +- `GoAI` β€” natural-language command dispatcher (translates English into go.nvim commands via Copilot/OpenAI) +- `GoCmtAI` β€” generate doc comments with AI for the declaration at cursor +- `GoDocAI` β€” AI-powered documentation: find symbols by vague name and generate rich docs from source +- `GoCodeReview` β€” AI code review for files, selections, or diffs; results populate the quickfix list + +**Documentation & Navigation** +- `GoDoc` / `GoDocBrowser` β€” view docs in a float or browser +- `GoCheat` β€” cheat sheets from [cheat.sh](https://cheat.sh/) +- `GoAlt` / `GoAltV` / `GoAltS` β€” switch between test and implementation files +- `GoPkgOutline` / `GoPkgSymbols` β€” package-level symbol outlines + +**Comments & Docs** +- Auto-generate doc comments for packages, functions, structs, and interfaces (suppresses golint warnings) + +**Module & Workspace** +- `GoModTidy`, `GoModVendor`, `GoGet`, `GoWork`, etc. +- `Gomvp` β€” rename/move packages +- `GoVulnCheck` β€” run govulncheck for vulnerability scanning + +**Syntax & Snippets** +- Treesitter-based syntax highlighting and textobjects +- Treesitter highlight injection for SQL, JSON, `go template`, and `gohtmltmpl` +- Feature-rich LuaSnip snippets included + +**Project & Configuration** +- Per-project setup via `.gonvim/init.lua` or `launch.json` +- Async jobs with libuv throughout ## Installation @@ -391,6 +411,20 @@ If no argument provided, fallback to lsp.hover() Similar to GoDoc, but open the browser with the doc link. If no argument provided, open doc for current function/package +## GoDocAI + +AI-powered documentation lookup. When you can't remember the exact package or function name, `GoDocAI` finds the +symbol using `go doc` and gopls workspace/symbol, then generates comprehensive documentation via AI. + +```vim +:GoDocAI Println +:GoDocAI http.ListenAndServe +:GoDocAI json Marshal +``` + +If no argument is given, uses the word under the cursor. Results are shown in a floating window. +Requires `ai = { enable = true }` in your go.nvim setup. + ## GoPkgOutline A symbol outline for all symbols (var, const, func, struct, interface etc) inside a package You can still use navigator @@ -540,9 +574,10 @@ The code will be: type GoLintComplaining struct{} ``` -| command | Description | -| ------- | ----------- | -| GoCmt | Add comment | +| command | Description | +| -------- | ------------------------------------------------------------------------ | +| GoCmt | Add comment | +| GoCmtAI | Generate doc comment using AI (Copilot or OpenAI) for declaration at cursor. Requires `ai = { enable = true }` | ## GoMod Commands @@ -725,6 +760,31 @@ if err != nil { | labels | | | outline | | +### AI Code Review + +`GoCodeReview` uses an LLM (Copilot or OpenAI-compatible) to review Go code and populate the quickfix list with +actionable findings (errors, warnings, suggestions). + +| Command | Description | +| ----------------------------- | ------------------------------------------------------------------ | +| GoCodeReview | Review the entire current file | +| :'<,'>GoCodeReview | Review the visual selection only | +| GoCodeReview -d | Review only changes (diff) against the default branch (main/master)| +| GoCodeReview -d develop | Review only changes (diff) against a specific branch | + +Requires `ai = { enable = true }` in your go.nvim setup. Results are loaded into the quickfix list. + +### AI Documentation + +`GoDocAI` finds a Go symbol by vague/partial name and generates rich AI documentation from its source. + +| Command | Description | +| ----------------------------- | ------------------------------------------------------------------ | +| GoDocAI {query} | Find symbol and generate AI documentation in a floating window | +| GoDocAI | Use word under cursor as query | + +Requires `ai = { enable = true }` in your go.nvim setup. + ### Debug Commands | Command | Description | @@ -857,7 +917,7 @@ require('go').setup({ -- go_input = require('guihua.input').input -- set to vim.ui.input to disable guihua input -- go_select = require('guihua.select').select -- vim.ui.select to disable guihua select lsp_document_formatting = true, - -- set to true: use gopls to format + -- set to true: use gopls to forβˆ‚mat -- false if you want to use other formatter tool(e.g. efm, nulls) lsp_inlay_hints = { enable = true, -- this is the only field apply to neovim > 0.10 @@ -892,6 +952,14 @@ require('go').setup({ -- can also set to a list of colors to define colors to choose from -- e.g {'#D8DEE9', '#5E81AC', '#88C0D0', '#EBCB8B', '#A3BE8C', '#B48EAD'} }, + ai = { + enable = false, -- set to true to enable AI features (GoAI, GoCmtAI) + provider = 'copilot', -- 'copilot' or 'openai' (any OpenAI-compatible endpoint) + model = nil, -- model name, default: 'gpt-4o' for copilot, 'gpt-4o-mini' for openai + api_key_env = 'OPENAI_API_KEY', -- env var name that holds the API key, env only! DO NOT put your key here. + base_url = nil, -- for openai-compatible APIs, e.g.: 'https://api.openai.com/v1' + confirm = true, -- confirm before executing the translated command + }, trouble = false, -- true: use trouble to open quickfix test_efm = false, -- errorfomat for quickfix, default mix mode, set to true will be efm only luasnip = false, -- enable included luasnip snippets. you can also disable while add lua/snips folder to luasnip load diff --git a/doc/go.txt b/doc/go.txt index c479333bf..305b94782 100644 --- a/doc/go.txt +++ b/doc/go.txt @@ -556,6 +556,44 @@ AI-POWERED ~ < Configure via the |ai| option. +:GoCodeReview [-d [{branch}]] *:GoCodeReview* + Review Go code with AI and populate the quickfix list with + actionable findings (errors, warnings, suggestions). + Uses the configured AI provider (see |ai| option). + + Modes: + :GoCodeReview Review the entire current file. + :'<,'>GoCodeReview Review the visual selection only. + :GoCodeReview -d Review only changes (diff) against + the default branch (main/master, + auto-detected). + :GoCodeReview -d develop + Review changes against a specific + branch. + + Findings are parsed into vim quickfix format with severity: + error (E), warning (W), or suggestion (I). + +:GoDocAI [{query}] *:GoDocAI* + Find a Go function, type, or symbol by a vague or partial name + and generate rich AI documentation from its source code. + Uses multiple strategies to locate the symbol: + 1. `go doc` / `go doc -src` for exact or partial matches + 2. gopls workspace/symbol for fuzzy lookup + + The AI then produces comprehensive Markdown documentation + (signature, description, parameters, return values, examples) + displayed in a floating window. + + If no argument is given, uses the word under the cursor. + Requires `ai = { enable = true }` (see |ai| option). + + Examples: > + :GoDocAI Println + :GoDocAI http.ListenAndServe + :GoDocAI json Marshal +< + ============================================================================== OPTIONS *go-nvim-options* @@ -642,7 +680,7 @@ You can setup go.nvim with following options: on_stderr = function(err, data) end, on_exit = function(code, signal, output) end, ai = { *ai* - enable = false, -- set to true to enable AI features (GoAI, GoCmtAI) + enable = false, -- set to true to enable AI features (GoAI, GoCmtAI, GoCodeReview, GoDocAI) provider = "copilot", -- "copilot" or "openai" model = nil, -- default: "gpt-4o" (copilot), "gpt-4o-mini" (openai) api_key_env = "OPENAI_API_KEY", -- env var name that holds the API key diff --git a/lua/go.lua b/lua/go.lua index 65793ebc9..2dddbeb90 100644 --- a/lua/go.lua +++ b/lua/go.lua @@ -165,6 +165,14 @@ _GO_NVIM_CFG = { end, -- callback for jobexit, output : string iferr_vertical_shift = 4, -- defines where the cursor will end up vertically from the begining of if err statement after GoIfErr command iferr_less_highlight = false, -- set to true to make 'if err != nil' statements less highlighted (grayed out) + ai = { + enable = false, -- set to true to enable AI features (GoAI, GoCmtAI) + provider = 'copilot', -- 'copilot' or 'openai' (any OpenAI-compatible endpoint) + model = nil, -- model name, default: 'gpt-4o' for copilot, 'gpt-4o-mini' for openai + api_key_env = 'OPENAI_API_KEY', -- env var name that holds the API key + base_url = nil, -- for openai-compatible APIs, e.g.: 'https://api.openai.com/v1' + confirm = true, -- confirm before executing the translated command + }, } -- TODO: nvim_{add,del}_user_command https://github.com/neovim/neovim/pull/16752 diff --git a/lua/go/ai.lua b/lua/go/ai.lua index 5156e5460..608a695ba 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -33,7 +33,7 @@ for _, c in ipairs({ 'GoMockGen', 'GoEnv', 'GoProject', 'GoToggleInlay', 'GoVulnCheck', 'GoNew', 'Gomvp', 'Ginkgo', 'GinkgoFunc', 'GinkgoFile', - 'GoGopls', 'GoCmtAI', 'GoCodeReview', + 'GoGopls', 'GoCmtAI', 'GoCodeReview', 'GoDocAI', }) do valid_cmd_set[c] = true end @@ -185,6 +185,7 @@ GOPLS LSP COMMANDS (via GoGopls [json_args]): AI-POWERED: - GoCmtAI β€” Generate doc comment for the declaration at cursor using AI - GoCodeReview β€” Review the current Go file (or visual selection) with AI; outputs findings to the vim quickfix list +- GoDocAI [query] β€” Find a function/type by vague name and generate rich AI documentation from its source code ]] local system_prompt = [[You are a command translator for go.nvim, a Neovim plugin for Go development. diff --git a/lua/go/commands.lua b/lua/go/commands.lua index eac33b7ce..c877b1579 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -664,5 +664,14 @@ return { return { '-d', '--diff' } end, }) + + create_cmd('GoDocAI', function(opts) + require('go.godoc').run_ai(opts) + end, { + nargs = '*', + complete = function(a, l) + return package.loaded.go.doc_complete(a, l) + end, + }) end, } diff --git a/lua/go/godoc.lua b/lua/go/godoc.lua index e55927637..bd5b609a3 100644 --- a/lua/go/godoc.lua +++ b/lua/go/godoc.lua @@ -202,4 +202,200 @@ m.run = function(fargs) }), setup end + +-- ─── GoDocAI ───────────────────────────────────────────────────────────────── + +local doc_ai_system_prompt = [[You are a Go documentation expert. The user will give you a vague or partial query about a Go function, method, type, or package, along with source code and/or doc output that was found. + +Your task is to produce clear, comprehensive, human-readable documentation in Markdown format for the matched symbol(s). + +For each symbol found, include: +1. **Signature** β€” the full function/method/type signature in a Go code block +2. **Description** β€” a clear explanation of what it does, its purpose, and typical usage +3. **Parameters** β€” describe each parameter (if applicable) +4. **Return values** β€” describe each return value (if applicable) +5. **Example** β€” a short usage example in a Go code block (if helpful) +6. **Notes** β€” any caveats, common mistakes, or related functions worth mentioning + +If the source code includes existing comments/doc, incorporate and expand on them. +If multiple symbols match, document each one separately. +Keep the output concise but informative. Do NOT include any preamble like "Here is the documentation". +]] + +--- Try to find source code for a symbol using multiple strategies: +--- 1. `go doc -src` for an exact or partial match +--- 2. `go doc` (without -src) for doc-only output +--- 3. gopls workspace/symbol for fuzzy lookup, then read the source +--- Returns (source_text, symbol_label) or (nil, error_msg) +local function find_symbol_source(query, callback) + -- Strategy 1: try `go doc -all` first to get doc text + local doc_cmd = string.format('go doc -all %s 2>/dev/null', vim.fn.shellescape(query)) + local doc_text = vim.fn.system(doc_cmd) + local doc_ok = (vim.v.shell_error == 0 and doc_text and vim.trim(doc_text) ~= '') + + -- Strategy 2: try `go doc -src` to get source code + local src_cmd = string.format('go doc -src %s 2>/dev/null', vim.fn.shellescape(query)) + local src_text = vim.fn.system(src_cmd) + local src_ok = (vim.v.shell_error == 0 and src_text and vim.trim(src_text) ~= '') + + if doc_ok or src_ok then + local combined = '' + if doc_ok then + combined = combined .. '--- go doc output ---\n' .. vim.trim(doc_text) .. '\n\n' + end + if src_ok then + combined = combined .. '--- source code ---\n' .. vim.trim(src_text) + end + callback(combined, query) + return + end + + -- Strategy 3: use gopls workspace/symbol for fuzzy matching + local clients = vim.lsp.get_clients({ bufnr = 0, name = 'gopls' }) + if not clients or #clients == 0 then + callback(nil, 'no documentation found for "' .. query .. '" (go doc failed and gopls not available)') + return + end + + vim.lsp.buf_request(0, 'workspace/symbol', { query = query }, function(err, result) + if err or not result or #result == 0 then + callback(nil, 'no symbols found matching "' .. query .. '"') + return + end + + vim.schedule(function() + -- Collect up to 5 best matches + local matches = {} + for i, sym in ipairs(result) do + if i > 5 then + break + end + local loc = sym.location + if loc and loc.uri then + local fpath = vim.uri_to_fname(loc.uri) + local line = (loc.range and loc.range.start and loc.range.start.line) or 0 + table.insert(matches, { + name = sym.name, + container = sym.containerName or '', + filepath = fpath, + line = line, + }) + end + end + + if #matches == 0 then + callback(nil, 'no readable symbols found for "' .. query .. '"') + return + end + + -- Read source around each match + local parts = {} + for _, match in ipairs(matches) do + local ok_file, file_lines = pcall(function() + return vim.fn.readfile(match.filepath) + end) + if ok_file and file_lines then + local start_line = math.max(0, match.line - 2) + local end_line = math.min(#file_lines, match.line + 50) + local snippet = {} + for l = start_line + 1, end_line do + table.insert(snippet, file_lines[l]) + end + local label = match.name + if match.container ~= '' then + label = match.container .. '.' .. match.name + end + table.insert(parts, string.format( + '--- %s (from %s:%d) ---\n%s', + label, + vim.fn.fnamemodify(match.filepath, ':~:.'), + match.line + 1, + table.concat(snippet, '\n') + )) + end + end + + if #parts == 0 then + callback(nil, 'found symbols but could not read source for "' .. query .. '"') + return + end + + callback(table.concat(parts, '\n\n'), query) + end) + end) +end + +--- Entry point for :GoDocAI {query} +--- Finds the symbol using go doc / gopls, then generates rich AI documentation. +--- @param opts table Standard nvim command opts (fargs) +m.run_ai = function(opts) + local fargs = (type(opts) == 'table' and opts.fargs) or opts or {} + local query = vim.trim(table.concat(fargs, ' ')) + + if query == '' then + -- Fallback: use the word under cursor + query = vim.fn.expand('') + if not query or query == '' then + vim.notify('go.nvim [DocAI]: please provide a query or place cursor on a symbol', vim.log.levels.WARN) + return + end + end + + local cfg = _GO_NVIM_CFG.ai or {} + if not cfg.enable then + vim.notify( + 'go.nvim [DocAI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoDocAI', + vim.log.levels.WARN + ) + return + end + + vim.notify('go.nvim [DocAI]: searching for "' .. query .. '" …', vim.log.levels.INFO) + + find_symbol_source(query, function(source, label) + if not source then + vim.notify('go.nvim [DocAI]: ' .. (label or 'symbol not found'), vim.log.levels.WARN) + return + end + + local user_msg = string.format('Query: %s\n\n%s', label, source) + + vim.notify('go.nvim [DocAI]: generating documentation …', vim.log.levels.INFO) + + require('go.ai').request(doc_ai_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) + resp = vim.trim(resp) + if resp == '' then + vim.notify('go.nvim [DocAI]: AI returned empty response', vim.log.levels.WARN) + return + end + + -- Display in a floating window + local lines = vim.split(resp, '\n', { plain = true }) + local height = math.min(#lines + 2, math.floor(vim.api.nvim_get_option('lines') * 0.8)) + local width = math.min(80, math.floor(vim.api.nvim_get_option('columns') * 0.8)) + -- Find the longest line to set width + for _, line in ipairs(lines) do + if #line > width then + width = math.min(#line + 2, math.floor(vim.api.nvim_get_option('columns') * 0.9)) + end + end + + local float_config = { + close_events = { 'CursorMoved', 'CursorMovedI', 'BufHidden', 'InsertCharPre' }, + focusable = true, + border = 'rounded', + width = width, + height = height, + max_width = math.floor(vim.api.nvim_get_option('columns') * 0.9), + max_height = math.floor(vim.api.nvim_get_option('lines') * 0.9), + offset_x = (vim.api.nvim_get_option('columns') - width) / 2 - 1, + offset_y = math.max(3, (vim.api.nvim_get_option('lines') - height) / 2 - 1) - 1, + relative = 'editor', + title = 'GoDocAI: ' .. query, + } + vim.lsp.util.open_floating_preview(lines, 'markdown', float_config) + end) + end) +end + return m From 9588c32c2f32b2b9d73f21bd89224a5344e07a26 Mon Sep 17 00:00:00 2001 From: ray-x Date: Thu, 5 Mar 2026 15:05:49 +1100 Subject: [PATCH 06/21] adding MCP supports --- README.md | 43 +++-- lua/go/ai.lua | 20 ++- lua/go/commands.lua | 50 +++++- lua/go/mcp/client.lua | 162 +++++++++++++++++ lua/go/mcp/context.lua | 386 +++++++++++++++++++++++++++++++++++++++++ lua/go/mcp/init.lua | 27 +++ lua/go/mcp/review.lua | 146 ++++++++++++++++ 7 files changed, 808 insertions(+), 26 deletions(-) create mode 100644 lua/go/mcp/client.lua create mode 100644 lua/go/mcp/context.lua create mode 100644 lua/go/mcp/init.lua create mode 100644 lua/go/mcp/review.lua diff --git a/README.md b/README.md index aa7db0254..c6426777c 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ possible. PR & Suggestions are welcome. The plugin covers most features required for a gopher. **LSP & Navigation** + - gopls commands: fillstruct, organize imports, list modules/packages, gc_details, generate, change signature, etc. - All GoToXxx (reference, implementation, definition, doc, peek code/doc, etc.) via gopls/lspconfig. Check [navigator.lua](https://github.com/ray-x/navigator.lua) for a floating UI experience @@ -14,6 +15,7 @@ The plugin covers most features required for a gopher. - CodeLens & CodeAction support **Build, Test & Coverage** + - Async build/make/test with libuv job wrapper - Test with `go test`, [gotestsum](https://github.com/gotestyourself/gotestsum), or [ginkgo](https://github.com/onsi/ginkgo) β€” including floaterm support - Generate unit tests with [gotests](https://github.com/cweill/gotests) (table-driven, testify) @@ -21,6 +23,7 @@ The plugin covers most features required for a gopher. - Smart build-tag detection for debug/test runs (e.g. `//go:build integration`) **Code Generation & Refactoring** + - `GoIfErr`, `GoFillStruct`, `GoFillSwitch`, `GoFixPlurals`, `GoGenReturn` β€” powered by treesitter/go AST - `GoImpl` β€” generate interface method stubs - `GoEnum` β€” generate enum helpers @@ -30,39 +33,47 @@ The plugin covers most features required for a gopher. - Struct tag management with [gomodifytags](https://github.com/fatih/gomodifytags) **Formatting & Linting** + - Format via LSP (gopls) or CLI (`gofumpt`, `goimports`, `golines`) - Lint with golangci-lint (v2) β€” LSP diagnostics or async background checks **Debugging** + - Dlv debug with [nvim-dap](https://github.com/mfussenegger/nvim-dap) and [nvim-dap-ui](https://github.com/rcarriga/nvim-dap-ui) β€” zero-config Go adapter included - Load VSCode `launch.json` configurations **AI-Powered** -- `GoAI` β€” natural-language command dispatcher (translates English into go.nvim commands via Copilot/OpenAI) + +- `GoAI` β€” natural-language command dispatcher (translates natural-language into go.nvim commands via Copilot/OpenAI) - `GoCmtAI` β€” generate doc comments with AI for the declaration at cursor - `GoDocAI` β€” AI-powered documentation: find symbols by vague name and generate rich docs from source - `GoCodeReview` β€” AI code review for files, selections, or diffs; results populate the quickfix list **Documentation & Navigation** + - `GoDoc` / `GoDocBrowser` β€” view docs in a float or browser - `GoCheat` β€” cheat sheets from [cheat.sh](https://cheat.sh/) - `GoAlt` / `GoAltV` / `GoAltS` β€” switch between test and implementation files - `GoPkgOutline` / `GoPkgSymbols` β€” package-level symbol outlines **Comments & Docs** -- Auto-generate doc comments for packages, functions, structs, and interfaces (suppresses golint warnings) + +- Auto-generate doc comments for packages, functions, structs, and interfaces (suppresses golint warnings) **Module & Workspace** + - `GoModTidy`, `GoModVendor`, `GoGet`, `GoWork`, etc. - `Gomvp` β€” rename/move packages - `GoVulnCheck` β€” run govulncheck for vulnerability scanning **Syntax & Snippets** + - Treesitter-based syntax highlighting and textobjects - Treesitter highlight injection for SQL, JSON, `go template`, and `gohtmltmpl` - Feature-rich LuaSnip snippets included **Project & Configuration** + - Per-project setup via `.gonvim/init.lua` or `launch.json` - Async jobs with libuv throughout @@ -574,10 +585,10 @@ The code will be: type GoLintComplaining struct{} ``` -| command | Description | -| -------- | ------------------------------------------------------------------------ | -| GoCmt | Add comment | -| GoCmtAI | Generate doc comment using AI (Copilot or OpenAI) for declaration at cursor. Requires `ai = { enable = true }` | +| command | Description | +| ------- | -------------------------------------------------------------------------------------------------------------- | +| GoCmt | Add comment | +| GoCmtAI | Generate doc comment using AI (Copilot or OpenAI) for declaration at cursor. Requires `ai = { enable = true }` | ## GoMod Commands @@ -765,12 +776,12 @@ if err != nil { `GoCodeReview` uses an LLM (Copilot or OpenAI-compatible) to review Go code and populate the quickfix list with actionable findings (errors, warnings, suggestions). -| Command | Description | -| ----------------------------- | ------------------------------------------------------------------ | -| GoCodeReview | Review the entire current file | -| :'<,'>GoCodeReview | Review the visual selection only | -| GoCodeReview -d | Review only changes (diff) against the default branch (main/master)| -| GoCodeReview -d develop | Review only changes (diff) against a specific branch | +| Command | Description | +| ----------------------- | ------------------------------------------------------------------- | +| GoCodeReview | Review the entire current file | +| :'<,'>GoCodeReview | Review the visual selection only | +| GoCodeReview -d | Review only changes (diff) against the default branch (main/master) | +| GoCodeReview -d develop | Review only changes (diff) against a specific branch | Requires `ai = { enable = true }` in your go.nvim setup. Results are loaded into the quickfix list. @@ -778,10 +789,10 @@ Requires `ai = { enable = true }` in your go.nvim setup. Results are loaded into `GoDocAI` finds a Go symbol by vague/partial name and generates rich AI documentation from its source. -| Command | Description | -| ----------------------------- | ------------------------------------------------------------------ | -| GoDocAI {query} | Find symbol and generate AI documentation in a floating window | -| GoDocAI | Use word under cursor as query | +| Command | Description | +| --------------- | -------------------------------------------------------------- | +| GoDocAI {query} | Find symbol and generate AI documentation in a floating window | +| GoDocAI | Use word under cursor as query | Requires `ai = { enable = true }` in your go.nvim setup. diff --git a/lua/go/ai.lua b/lua/go/ai.lua index 608a695ba..2c3c872b8 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -434,6 +434,7 @@ local function send_copilot_raw(sys_prompt, user_msg, opts, callback) get_copilot_api_token(oauth, function(token) local cfg = _GO_NVIM_CFG.ai or {} local model = cfg.model or 'gpt-4o' + log('build_body with model', model, 'sys_prompt ', sys_prompt, 'user_msg', user_msg, 'opts', opts) local body = build_body(model, sys_prompt, user_msg, opts) local nvim_ver = string.format('%s.%s.%s', vim.version().major, vim.version().minor, vim.version().patch) local headers = { @@ -756,13 +757,13 @@ local function handle_review_response(response, filename) end if #qflist == 0 then - vim.notify('go.nvim [CodeReview]: great job! No issues found.', vim.log.levels.INFO) + vim.notify('[GoCodeReview]: great job! No issues found.', vim.log.levels.INFO) return end vim.fn.setqflist({}, 'r', { title = 'GoCodeReview', items = qflist }) vim.cmd('copen') - vim.notify(string.format('go.nvim [CodeReview]: %d issue(s) added to quickfix', #qflist), vim.log.levels.INFO) + vim.notify(string.format('[GoCodeReview]: %d issue(s) added to quickfix', #qflist), vim.log.levels.INFO) end --- Entry point for :GoCodeReview [-d [branch]] @@ -798,10 +799,10 @@ function M.code_review(opts) if diff_mode then local branch = diff_branch or detect_default_branch() - vim.notify('go.nvim [CodeReview]: diffing against ' .. branch .. ' …', vim.log.levels.INFO) + vim.notify('[GoCodeReview]: diffing against ' .. branch .. ' …', vim.log.levels.INFO) get_git_diff(filename, branch, function(diff, err) if err then - vim.notify('go.nvim [CodeReview]: ' .. err, vim.log.levels.WARN) + vim.notify('[GoCodeReview]: ' .. err, vim.log.levels.WARN) return end local short_name = vim.fn.expand('%:t') @@ -827,7 +828,7 @@ function M.code_review(opts) end if #lines == 0 then - vim.notify('go.nvim [CodeReview]: buffer is empty', vim.log.levels.WARN) + vim.notify('[GoCodeReview]: buffer is empty', vim.log.levels.WARN) return end @@ -840,7 +841,7 @@ function M.code_review(opts) local short_name = vim.fn.expand('%:t') local user_msg = string.format('File: %s\n\n```go\n%s\n```', short_name, code) - vim.notify('go.nvim [CodeReview]: reviewing …', vim.log.levels.INFO) + vim.notify('[GoCodeReview]: reviewing …', vim.log.levels.INFO) M.request(code_review_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) handle_review_response(resp, filename) @@ -857,7 +858,7 @@ function M.request(sys_prompt, user_msg, opts, callback) opts = opts or {} local cfg = _GO_NVIM_CFG.ai or {} if not cfg.enable then - vim.notify('go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup', vim.log.levels.WARN) + vim.notify('[GoCodeReview]: AI is disabled. Set ai = { enable = true } in go.nvim setup', vim.log.levels.WARN) return end local provider = cfg.provider or 'copilot' @@ -867,8 +868,11 @@ function M.request(sys_prompt, user_msg, opts, callback) elseif provider == 'openai' then send_openai_raw(sys_prompt, user_msg, opts, callback) else - vim.notify('go.nvim [AI]: unknown provider "' .. provider .. '"', vim.log.levels.ERROR) + vim.notify('[GoCodeReview]: unknown provider "' .. provider .. '"', vim.log.levels.ERROR) end end +M.code_review_system_prompt = code_review_system_prompt +M.diff_review_system_prompt = diff_review_system_prompt + return M diff --git a/lua/go/commands.lua b/lua/go/commands.lua index c877b1579..76c428c86 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -655,8 +655,54 @@ return { require('go.ai').run(opts) end, { nargs = '*', range = true }) - create_cmd('GoCodeReview', function(opts) - require('go.ai').code_review(opts) + ---@param args table the opts table passed by nvim_create_user_command +---@return table parsed options +local function parse_review_args(args) + local opts = {} + local fargs = args.fargs or {} + + -- Check for visual range selection + if args.range and args.range == 2 then + opts.visual = true + local start_line = args.line1 + local end_line = args.line2 + local bufnr = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false) + opts.lines = table.concat(lines, '\n') + opts.start_line = start_line + opts.end_line = end_line + end + + for i, arg in ipairs(fargs) do + if arg == '-d' or arg == '--diff' then + opts.diff = true + -- Next arg might be branch name + if fargs[i + 1] and not fargs[i + 1]:match('^%-') then + opts.branch = fargs[i + 1] + end + end + end + + -- Default branch if diff mode but no branch specified + if opts.diff and not opts.branch then + opts.branch = 'master' + end + + return opts +end + + create_cmd('GoCodeReview', function(args) + + local opts = parse_review_args(args) + + -- Use MCP-enhanced review when available + local go_cfg = require('go').config() or {} + if go_cfg.mcp and go_cfg.mcp.enable then + require('go.mcp.review').review(opts) + else + -- Fallback to existing review without MCP + require('go.ai.review').review(opts) + end end, { nargs = '*', range = true, diff --git a/lua/go/mcp/client.lua b/lua/go/mcp/client.lua new file mode 100644 index 000000000..8b94c4968 --- /dev/null +++ b/lua/go/mcp/client.lua @@ -0,0 +1,162 @@ +local M = {} +local uv = vim.uv or vim.loop +local log = require('go.utils').log +local json = vim.json + +---@class McpClient +---@field handle uv_process_t +---@field stdin uv_pipe_t +---@field stdout uv_pipe_t +---@field pending table +---@field next_id number +---@field buffer string +local McpClient = {} +McpClient.__index = McpClient + +function M.new(opts) + local self = setmetatable({}, McpClient) + self.pending = {} + self.next_id = 1 + self.buffer = '' + self.ready = false + self:_start(opts) + return self +end + +function McpClient:_start(opts) + local cmd = opts.cmd or { 'gopls', 'mcp' } + self.stdin = uv.new_pipe(false) + self.stdout = uv.new_pipe(false) + local stderr = uv.new_pipe(false) + + local args = {} + for i = 2, #cmd do + args[i - 1] = cmd[i] + end + + self.handle = uv.spawn(cmd[1], { + args = args, + stdio = { self.stdin, self.stdout, stderr }, + cwd = opts.root_dir or vim.fn.getcwd(), + }, function(code, signal) + log('gopls mcp exited', code, signal) + end) + + if not self.handle then + error('Failed to start gopls mcp: ' .. cmd[1]) + end + + -- Read stdout for JSON-RPC responses + self.stdout:read_start(function(err, data) + if err then + log('mcp read error:', err) + return + end + if data then + self:_on_data(data) + end + end) + + stderr:read_start(function(_, data) + if data then + log('mcp stderr:', data) + end + end) + + -- Send MCP initialize + self:request('initialize', { + protocolVersion = '2025-03-26', + capabilities = {}, + clientInfo = { name = 'go.nvim', version = '1.0.0' }, + }, function(err, result) + if not err then + self.ready = true + -- Send initialized notification + self:notify('notifications/initialized', {}) + log('MCP initialized, server:', result) + else + log('MCP init error:', err) + end + end) +end + +--- Parse MCP JSON-RPC messages (newline-delimited JSON) +function McpClient:_on_data(data) + self.buffer = self.buffer .. data + while true do + local nl = self.buffer:find('\n') + if not nl then + break + end + local line = self.buffer:sub(1, nl - 1) + self.buffer = self.buffer:sub(nl + 1) + + if #line > 0 then + local ok, msg = pcall(json.decode, line) + if ok and msg then + self:_handle_message(msg) + end + end + end +end + +function McpClient:_handle_message(msg) + if msg.id and self.pending[msg.id] then + local cb = self.pending[msg.id] + self.pending[msg.id] = nil + if msg.error then + cb(msg.error, nil) + else + cb(nil, msg.result) + end + end +end + +function McpClient:request(method, params, callback) + local id = self.next_id + self.next_id = self.next_id + 1 + self.pending[id] = callback + + local msg = json.encode({ + jsonrpc = '2.0', + id = id, + method = method, + params = params, + }) .. '\n' + + self.stdin:write(msg) +end + +function McpClient:notify(method, params) + local msg = json.encode({ + jsonrpc = '2.0', + method = method, + params = params, + }) .. '\n' + self.stdin:write(msg) +end + +--- Call an MCP tool +---@param tool_name string +---@param arguments table +---@param callback function(err, result) +function McpClient:call_tool(tool_name, arguments, callback) + self:request('tools/call', { + name = tool_name, + arguments = arguments, + }, callback) +end + +function McpClient:list_tools(callback) + self:request('tools/list', {}, callback) +end + +function McpClient:shutdown() + if self.handle then + self.stdin:close() + self.stdout:close() + self.handle:kill('sigterm') + end +end + +return M \ No newline at end of file diff --git a/lua/go/mcp/context.lua b/lua/go/mcp/context.lua new file mode 100644 index 000000000..484829fc7 --- /dev/null +++ b/lua/go/mcp/context.lua @@ -0,0 +1,386 @@ +local M = {} +local log = require('go.utils').log + +--- Parse a unified diff to extract changed file/line info +---@param diff_text string +---@return table[] list of {file, start_line, end_line} +function M.parse_diff_hunks(diff_text) + local hunks = {} + local current_file = nil + for line in diff_text:gmatch('[^\n]+') do + local file = line:match('^%+%+%+ b/(.+%.go)$') + if file then + current_file = file + end + local start, count = line:match('^@@ %-[%d,]+ %+(%d+),?(%d*) @@') + if start and current_file then + count = tonumber(count) or 1 + table.insert(hunks, { + file = current_file, + start_line = tonumber(start), + end_line = tonumber(start) + count - 1, + }) + end + end + return hunks +end + +--- Load a file into a buffer and ensure treesitter + LSP are ready +---@param filepath string absolute path +---@return number|nil bufnr +local function ensure_buffer(filepath) + if vim.fn.filereadable(filepath) ~= 1 then + return nil + end + local bufnr = vim.fn.bufadd(filepath) + vim.fn.bufload(bufnr) + if vim.bo[bufnr].filetype == '' then + vim.bo[bufnr].filetype = 'go' + end + return bufnr +end + +--- Read a specific line from a file (1-indexed) +---@param filepath string absolute path +---@param lnum number 1-indexed line number +---@return string|nil +local function read_line(filepath, lnum) + -- Try from loaded buffer first + local bufnr = vim.fn.bufnr(filepath) + if bufnr ~= -1 and vim.api.nvim_buf_is_loaded(bufnr) then + local lines = vim.api.nvim_buf_get_lines(bufnr, lnum - 1, lnum, false) + if lines and lines[1] then + return vim.trim(lines[1]) + end + end + -- Fall back to reading from disk + local f = io.open(filepath, 'r') + if not f then + return nil + end + local i = 0 + for line in f:lines() do + i = i + 1 + if i == lnum then + f:close() + return vim.trim(line) + end + end + f:close() + return nil +end + +--- Check if a filename is a test file +---@param fname string +---@return boolean +local function is_test_file(fname) + return fname:match('_test%.go$') ~= nil +end + +--- Format a reference location, including line text for non-test files +---@param uri string LSP URI +---@param line number 0-indexed +---@return string +local function format_ref_location(uri, line) + local fpath = vim.uri_to_fname(uri) + local fname = vim.fn.fnamemodify(fpath, ':.') + local lnum = line + 1 + + if is_test_file(fname) then + return string.format(' %s:%d', fname, lnum) + end + + local text = read_line(fpath, lnum) + if text then + return string.format(' %s:%d `%s`', fname, lnum, text) + end + return string.format(' %s:%d', fname, lnum) +end + +--- Format a caller location, including line text for non-test files +---@param uri string LSP URI +---@param line number 0-indexed +---@param caller_name string +---@return string +local function format_caller_location(uri, line, caller_name) + local fpath = vim.uri_to_fname(uri) + local fname = vim.fn.fnamemodify(fpath, ':.') + local lnum = line + 1 + + if is_test_file(fname) then + return string.format(' %s:%d β€” %s()', fname, lnum, caller_name) + end + + local text = read_line(fpath, lnum) + if text then + return string.format(' %s:%d β€” %s() `%s`', fname, lnum, caller_name, text) + end + return string.format(' %s:%d β€” %s()', fname, lnum, caller_name) +end + +--- Use treesitter to find symbols (functions, types, methods) at given lines +---@param bufnr number +---@param start_line number (0-indexed) +---@param end_line number (0-indexed) +---@return table[] symbols [{name, kind, line, col}] +function M.find_symbols_in_range(bufnr, start_line, end_line) + local symbols = {} + local ok, parser = pcall(vim.treesitter.get_parser, bufnr, 'go') + if not ok or not parser then + return symbols + end + local trees = parser:parse() + if not trees or not trees[1] then + return symbols + end + local root = trees[1]:root() + + local query_text = [[ + (function_declaration name: (identifier) @func_name) @func + (method_declaration name: (field_identifier) @method_name) @method + (type_declaration (type_spec name: (type_identifier) @type_name)) @type + ]] + + local qok, query = pcall(vim.treesitter.query.parse, 'go', query_text) + if not qok then + return symbols + end + + for id, node, _ in query:iter_captures(root, bufnr, start_line, end_line + 1) do + local name = query.captures[id] + if name == 'func_name' or name == 'method_name' or name == 'type_name' then + local text = vim.treesitter.get_node_text(node, bufnr) + local row, col = node:start() + local kind = name:match('^(%w+)_') + table.insert(symbols, { name = text, kind = kind, line = row, col = col }) + end + end + return symbols +end + +--- Get symbol context using the EXISTING running LSP client (no second gopls) +---@param bufnr number +---@param line number 0-indexed +---@param col number 0-indexed +---@param callback function(context_string) +function M.get_symbol_context_via_lsp(bufnr, line, col, callback) + local results = {} + local pending = 3 + + -- Find an attached gopls client for this buffer + local clients = vim.lsp.get_clients({ bufnr = bufnr, name = 'gopls' }) + if #clients == 0 then + clients = vim.lsp.get_clients({ name = 'gopls' }) + if #clients == 0 then + callback('(no gopls LSP client available)') + return + end + vim.lsp.buf_attach_client(bufnr, clients[1].id) + end + + local function make_params() + return { + textDocument = { uri = vim.uri_from_bufnr(bufnr) }, + position = { line = line, character = col }, + } + end + + local function check_done() + pending = pending - 1 + if pending <= 0 then + callback(table.concat(results, '\n')) + end + end + + -- 1. References + local ref_params = make_params() + ref_params.context = { includeDeclaration = false } + vim.lsp.buf_request(bufnr, 'textDocument/references', ref_params, function(err, result) + if not err and result and #result > 0 then + local refs = {} + local seen = {} + for _, ref in ipairs(result) do + local fname = vim.fn.fnamemodify(vim.uri_to_fname(ref.uri), ':.') + local key = string.format('%s:%d', fname, ref.range.start.line + 1) + if not seen[key] then + seen[key] = true + table.insert(refs, format_ref_location(ref.uri, ref.range.start.line)) + end + if #refs >= 20 then + table.insert(refs, string.format(' ... and %d more', #result - 20)) + break + end + end + table.insert(results, '### References (' .. #result .. '):\n' .. table.concat(refs, '\n')) + end + check_done() + end) + + -- 2. Incoming calls (who calls this?) + vim.lsp.buf_request(bufnr, 'textDocument/prepareCallHierarchy', make_params(), function(err, result) + if err or not result or #result == 0 then + check_done() + return + end + vim.lsp.buf_request(bufnr, 'callHierarchy/incomingCalls', { item = result[1] }, function(err2, calls) + if not err2 and calls and #calls > 0 then + local callers = {} + for _, call in ipairs(calls) do + table.insert(callers, format_caller_location( + call.from.uri, call.from.range.start.line, call.from.name + )) + if #callers >= 15 then + table.insert(callers, string.format(' ... and %d more', #calls - 15)) + break + end + end + table.insert(results, '### Callers (' .. #calls .. '):\n' .. table.concat(callers, '\n')) + end + check_done() + end) + end) + + -- 3. Implementations (for interfaces/methods) + vim.lsp.buf_request(bufnr, 'textDocument/implementation', make_params(), function(err, result) + if not err and result and #result > 0 then + local impls = {} + for _, impl in ipairs(result) do + table.insert(impls, format_ref_location(impl.uri, impl.range.start.line)) + if #impls >= 15 then + break + end + end + table.insert(results, '### Implementations (' .. #result .. '):\n' .. table.concat(impls, '\n')) + end + check_done() + end) +end + +-- Backward-compatible alias +M.get_symbol_context = M.get_symbol_context_via_lsp + +--- Gather semantic context for all changed symbols across diff files +--- Uses the EXISTING gopls LSP client instead of a separate MCP process +---@param diff_text string +---@param callback function(semantic_context: string) +function M.gather_diff_context(diff_text, callback) + local hunks = M.parse_diff_hunks(diff_text) + + if #hunks == 0 then + callback('(no changed Go symbols detected)') + return + end + + local by_file = {} + for _, hunk in ipairs(hunks) do + by_file[hunk.file] = by_file[hunk.file] or {} + table.insert(by_file[hunk.file], hunk) + end + + local all_context = {} + local files_pending = vim.tbl_count(by_file) + + if files_pending == 0 then + callback('(no changed Go files)') + return + end + + for file, file_hunks in pairs(by_file) do + local abs_path = vim.fn.getcwd() .. '/' .. file + local bufnr = ensure_buffer(abs_path) + + if not bufnr then + log('mcp/context: cannot load file', abs_path) + files_pending = files_pending - 1 + if files_pending == 0 then + callback(table.concat(all_context, '\n\n')) + end + goto continue + end + + local symbols = {} + for _, hunk in ipairs(file_hunks) do + local found = M.find_symbols_in_range(bufnr, hunk.start_line - 1, hunk.end_line - 1) + for _, sym in ipairs(found) do + symbols[sym.name] = sym + end + end + + local symbol_list = vim.tbl_values(symbols) + if #symbol_list == 0 then + table.insert(all_context, string.format('## File: %s\n(changed lines do not contain function/type declarations)', file)) + files_pending = files_pending - 1 + if files_pending == 0 then + callback(table.concat(all_context, '\n\n')) + end + else + local syms_pending = #symbol_list + for _, sym in ipairs(symbol_list) do + local header = string.format('## Symbol: `%s` (%s) in %s:%d', sym.name, sym.kind, file, sym.line + 1) + + M.get_symbol_context_via_lsp(bufnr, sym.line, sym.col, function(ctx) + if ctx and #ctx > 0 then + table.insert(all_context, header .. '\n' .. ctx) + else + table.insert(all_context, header .. '\n(no callers/references found β€” possibly unexported or unused)') + end + + syms_pending = syms_pending - 1 + if syms_pending == 0 then + files_pending = files_pending - 1 + if files_pending == 0 then + callback(table.concat(all_context, '\n\n')) + end + end + end) + end + end + + ::continue:: + end +end + +--- Gather context for all symbols in a single buffer (non-diff mode) +---@param bufnr number +---@param callback function(semantic_context: string) +function M.gather_buffer_context(bufnr, callback) + local line_count = vim.api.nvim_buf_line_count(bufnr) + local symbols = M.find_symbols_in_range(bufnr, 0, line_count - 1) + local file = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(bufnr), ':.') + + if #symbols == 0 then + callback('(no function/type declarations found in buffer)') + return + end + + if #symbols > 15 then + log('mcp/context: truncating symbols from', #symbols, 'to 15') + local truncated = {} + for i = 1, 15 do + truncated[i] = symbols[i] + end + symbols = truncated + end + + local all_ctx = {} + local pending = #symbols + + for _, sym in ipairs(symbols) do + local header = string.format('## Symbol: `%s` (%s) in %s:%d', sym.name, sym.kind, file, sym.line + 1) + + M.get_symbol_context_via_lsp(bufnr, sym.line, sym.col, function(ctx) + if ctx and #ctx > 0 then + table.insert(all_ctx, header .. '\n' .. ctx) + else + table.insert(all_ctx, header .. '\n(no callers/references found)') + end + + pending = pending - 1 + if pending == 0 then + callback(table.concat(all_ctx, '\n\n')) + end + end) + end +end + +return M \ No newline at end of file diff --git a/lua/go/mcp/init.lua b/lua/go/mcp/init.lua new file mode 100644 index 000000000..4f2ab5dbe --- /dev/null +++ b/lua/go/mcp/init.lua @@ -0,0 +1,27 @@ +local M = {_client = nil} +local client_mod = require('go.mcp.client') + +function M.setup(opts) + opts = opts or {} + M._config = { + gopls_cmd = opts.gopls_cmd or { 'gopls', 'mcp' }, + root_dir = opts.root_dir, + } +end + +function M.get_client() + if not M._client then + M._client = client_mod.new(M._config) + end + return M._client +end + +function M.shutdown() + if M._client then + M._client:shutdown() + M._client = nil + end +end + + +return M \ No newline at end of file diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua new file mode 100644 index 000000000..b4b097226 --- /dev/null +++ b/lua/go/mcp/review.lua @@ -0,0 +1,146 @@ +local M = {} +local mcp_context = require('go.mcp.context') +local log = require('go.utils').log + +--- Build the enriched prompt with diff + semantic context +---@param diff_text string the git diff +---@param semantic_context string gathered MCP context +---@param opts table +---@return string the full prompt for AI +local function build_enriched_prompt(diff_text, semantic_context, opts) + return string.format([[ +You are an expert Go code reviewer. Review the following code changes. + +You have access to **semantic context** gathered from gopls β€” this includes +information about callers, references, and interface implementations for +every changed symbol. Use this to assess the **full impact** of the changes: +- Are callers affected by signature changes? +- Do interface contracts still hold? +- Are there downstream consumers that might break? +- Are error handling patterns consistent with existing callers? + +## Git Diff +```diff +%s +``` + +## Semantic Context for Changed Symbols +%s + +## Instructions +1. Identify bugs, race conditions, security issues, and API contract violations. +2. Flag changes that break callers or interface implementations (you have the data). +3. Check error handling, nil safety, and resource cleanup. +4. Assess backward compatibility using the reference/caller information. +5. Suggest concrete improvements. + +Respond ONLY with a JSON array. Each element: +{"file":"","line":,"severity":"error|warning|info","message":""} +No markdown wrapping. Just the JSON array. +]], diff_text, semantic_context) +end + +--- Enhanced code review with semantic context from the running gopls LSP +---@param opts table {diff=bool, branch=string, visual=bool, lines=string} +function M.review(opts) + opts = opts or {} + local ai = require('go.ai') + + -- Get the diff or file content (reuse existing logic) + local code_text + if opts.diff then + local branch = opts.branch or 'main' + code_text = vim.fn.system({ 'git', 'diff', branch, '--', '*.go' }) + if vim.v.shell_error ~= 0 then + -- Try master if main doesn't exist + code_text = vim.fn.system({ 'git', 'diff', 'master', '--', '*.go' }) + end + elseif opts.visual and opts.lines then + code_text = opts.lines + else + local bufnr = vim.api.nvim_get_current_buf() + code_text = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), '\n') + end + + if not code_text or #code_text == 0 then + vim.notify('No code to review', vim.log.levels.WARN) + return + end + + vim.notify('go.nvim [Review]: gathering semantic context from gopls...', vim.log.levels.INFO) + + local function send_review(sys_prompt, semantic_ctx) + local prompt = build_enriched_prompt(code_text, semantic_ctx, opts) + ai.request( + sys_prompt, + prompt, + { max_tokens = 4096, temperature = 0 }, + function(response) + M._handle_review_response(response) + end + ) + end + + if opts.diff then + -- Use the NEW function from context.lua + mcp_context.gather_diff_context(code_text, function(semantic_ctx) + vim.schedule(function() + vim.notify('go.nvim [Review]: semantic context ready. Sending to AI...', vim.log.levels.INFO) + send_review(ai.diff_review_system_prompt, semantic_ctx) + end) + end) + else + local bufnr = vim.api.nvim_get_current_buf() + -- Use the NEW function from context.lua + mcp_context.gather_buffer_context(bufnr, function(semantic_ctx) + vim.schedule(function() + vim.notify('go.nvim [Review]: semantic context ready. Sending to AI...', vim.log.levels.INFO) + send_review(ai.code_review_system_prompt, semantic_ctx) + end) + end) + end +end + +--- Parse AI response and populate quickfix list +function M._handle_review_response(response) + vim.schedule(function() + if not response or #response == 0 then + vim.notify('No review findings', vim.log.levels.INFO) + return + end + + -- Strip markdown code fences if present + local cleaned = response:gsub('^```json%s*', ''):gsub('%s*```$', '') + local ok, findings = pcall(vim.json.decode, cleaned) + if not ok or type(findings) ~= 'table' then + vim.notify('Failed to parse AI review response', vim.log.levels.ERROR) + log('Raw response:', response) + return + end + + local qf_items = {} + for _, f in ipairs(findings) do + local severity_map = { + error = 'E', + warning = 'W', + info = 'I', + } + table.insert(qf_items, { + filename = f.file or vim.fn.expand('%'), + lnum = f.line or 1, + col = 1, + text = f.message or '', + type = severity_map[f.severity] or 'I', + }) + end + + vim.fn.setqflist(qf_items, 'r') + vim.fn.setqflist({}, 'a', { title = 'GoCodeReview (MCP-enhanced)' }) + if #qf_items > 0 then + vim.cmd('copen') + end + vim.notify(string.format('Code review: %d findings', #qf_items), vim.log.levels.INFO) + end) +end + +return M \ No newline at end of file From 49905ad8968698a460b3f3a1c6a5c71eb5e142b7 Mon Sep 17 00:00:00 2001 From: ray-x Date: Fri, 6 Mar 2026 15:10:31 +1100 Subject: [PATCH 07/21] add mcp options to gopls --- lua/go.lua | 4 + lua/go/ai.lua | 478 +++++++++++++++++++++++++++++++---------- lua/go/commands.lua | 93 ++++---- lua/go/mcp/client.lua | 5 +- lua/go/mcp/context.lua | 34 +-- lua/go/mcp/init.lua | 5 +- lua/go/mcp/review.lua | 173 ++++++++++++--- 7 files changed, 591 insertions(+), 201 deletions(-) diff --git a/lua/go.lua b/lua/go.lua index 2dddbeb90..792de87f9 100644 --- a/lua/go.lua +++ b/lua/go.lua @@ -173,6 +173,10 @@ _GO_NVIM_CFG = { base_url = nil, -- for openai-compatible APIs, e.g.: 'https://api.openai.com/v1' confirm = true, -- confirm before executing the translated command }, + mcp = { + enable = false, -- set to true to enable gopls MCP features + gopls_cmd = {'gopls', 'mcp'}, -- you can provide gopls path and cmd if it not in PATH, e.g. cmd = { "/home/ray/.local/nvim/data/lspinstall/go/gopls", "mcp" } + }, } -- TODO: nvim_{add,del}_user_command https://github.com/neovim/neovim/pull/16752 diff --git a/lua/go/ai.lua b/lua/go/ai.lua index 2c3c872b8..5ced591fc 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -11,32 +11,26 @@ local log = utils.log local _copilot_token = nil local _copilot_token_expires = 0 --- stylua: ignore +-- stylua: ignore start local valid_cmd_set = {} for _, c in ipairs({ - 'GoTest', 'GoTestFunc', 'GoTestFile', 'GoTestPkg', 'GoTestSubCase', 'GoTestSum', - 'GoAddTest', 'GoAddExpTest', 'GoAddAllTest', 'GoCoverage', - 'GoBuild', 'GoRun', 'GoGenerate', 'GoVet', 'GoLint', 'GoMake', 'GoStop', - 'GoFmt', 'GoImports', - 'GoIfErr', 'GoFillStruct', 'GoFillSwitch', 'GoFixPlurals', 'GoCmt', - 'GoImpl', 'GoEnum', 'GoGenReturn', 'GoJson2Struct', - 'GoAddTag', 'GoRmTag', 'GoClearTag', 'GoModifyTag', - 'GoModTidy', 'GoModVendor', 'GoModDnld', 'GoModGraph', 'GoModWhy', 'GoModInit', - 'GoGet', 'GoWork', - 'GoDoc', 'GoDocBrowser', 'GoAlt', 'GoAltV', 'GoAltS', - 'GoImplements', 'GoPkgOutline', 'GoPkgSymbols', 'GoListImports', 'GoCheat', - 'GoCodeAction', 'GoCodeLenAct', 'GoRename', 'GoGCDetails', - 'GoDebug', 'GoBreakToggle', 'GoBreakSave', 'GoBreakLoad', - 'GoDbgStop', 'GoDbgContinue', 'GoDbgKeys', 'GoCreateLaunch', - 'DapStop', 'DapRerun', 'BreakCondition', 'LogPoint', 'ReplRun', 'ReplToggle', 'ReplOpen', - 'GoInstallBinary', 'GoUpdateBinary', 'GoInstallBinaries', 'GoUpdateBinaries', 'GoTool', - 'GoMockGen', - 'GoEnv', 'GoProject', 'GoToggleInlay', 'GoVulnCheck', - 'GoNew', 'Gomvp', 'Ginkgo', 'GinkgoFunc', 'GinkgoFile', - 'GoGopls', 'GoCmtAI', 'GoCodeReview', 'GoDocAI', + 'GoTest', 'GoTestFunc', 'GoTestFile', 'GoTestPkg', 'GoTestSubCase', 'GoTestSum', 'GoAddTest', + 'GoAddExpTest', 'GoAddAllTest', 'GoCoverage', 'GoBuild', 'GoRun', 'GoGenerate', 'GoVet', + 'GoLint', 'GoMake', 'GoStop', 'GoFmt', 'GoImports', 'GoIfErr', 'GoFillStruct', + 'GoFillSwitch', 'GoFixPlurals', 'GoCmt', 'GoImpl', 'GoEnum', 'GoGenReturn', 'GoJson2Struct', + 'GoAddTag', 'GoRmTag', 'GoClearTag', 'GoModifyTag', 'GoModTidy', 'GoModVendor', 'GoModDnld', + 'GoModGraph', 'GoModWhy', 'GoModInit', 'GoGet', 'GoWork', 'GoDoc', 'GoDocBrowser', + 'GoAlt', 'GoAltV', 'GoAltS', 'GoImplements', 'GoPkgOutline', 'GoPkgSymbols', 'GoListImports', + 'GoCheat', 'GoCodeAction', 'GoCodeLenAct', 'GoRename', 'GoGCDetails', 'GoDebug', 'GoBreakToggle', + 'GoBreakSave', 'GoBreakLoad', 'GoDbgStop', 'GoDbgContinue', 'GoDbgKeys', 'GoCreateLaunch', 'DapStop', + 'DapRerun', 'BreakCondition', 'LogPoint', 'ReplRun', 'ReplToggle', 'ReplOpen', 'GoInstallBinary', + 'GoUpdateBinary', 'GoInstallBinaries', 'GoUpdateBinaries', 'GoTool', 'GoMockGen', 'GoEnv', + 'GoProject', 'GoToggleInlay', 'GoVulnCheck', 'GoNew', 'Gomvp', 'Ginkgo', + 'GinkgoFunc', 'GinkgoFile', 'GoGopls', 'GoCmtAI', 'GoCodeReview', 'GoDocAI', }) do valid_cmd_set[c] = true end +-- stylua: ignore end local command_catalog = [[ go.nvim Commands Reference: @@ -257,41 +251,39 @@ local function get_copilot_api_token(oauth_token, callback) return end - vim.system( - { - 'curl', '-s', - '--connect-timeout', '10', - '--max-time', '15', - '-w', '\n%{http_code}', - '-H', 'Authorization: token ' .. oauth_token, - '-H', 'Accept: application/json', - 'https://api.github.com/copilot_internal/v2/token', - }, - { text = true }, - function(result) - vim.schedule(function() - if result.code ~= 0 then - local msg = parse_curl_error(result.code, result.stderr) - vim.notify('go.nvim [AI]: Copilot token request failed: ' .. msg, vim.log.levels.ERROR) - return - end - local stdout = result.stdout or '' - local body, http_code = split_http_response(stdout) - if http_code ~= '200' then - vim.notify('go.nvim [AI]: Copilot token request returned HTTP ' .. http_code .. ': ' .. body:sub(1, 200), vim.log.levels.ERROR) - return - end - local ok, data = pcall(vim.json.decode, body) - if ok and data and data.token then - _copilot_token = data.token - _copilot_token_expires = (data.expires_at or 0) - 60 -- refresh 60s early - callback(data.token) - else - vim.notify('go.nvim [AI]: unexpected Copilot token response', vim.log.levels.ERROR) - end - end) - end - ) + -- stylua: ignore start + vim.system({ + 'curl', '-s', '--connect-timeout', '10', + '--max-time', '15', '-w', '\n%{http_code}', + '-H', 'Authorization: token ' .. oauth_token, '-H', 'Accept: application/json', + 'https://api.github.com/copilot_internal/v2/token', + }, { text = true }, function(result) + -- stylua: ignore end + vim.schedule(function() + if result.code ~= 0 then + local msg = parse_curl_error(result.code, result.stderr) + vim.notify('go.nvim [AI]: Copilot token request failed: ' .. msg, vim.log.levels.ERROR) + return + end + local stdout = result.stdout or '' + local body, http_code = split_http_response(stdout) + if http_code ~= '200' then + vim.notify( + 'go.nvim [AI]: Copilot token request returned HTTP ' .. http_code .. ': ' .. body:sub(1, 200), + vim.log.levels.ERROR + ) + return + end + local ok, data = pcall(vim.json.decode, body) + if ok and data and data.token then + _copilot_token = data.token + _copilot_token_expires = (data.expires_at or 0) - 60 -- refresh 60s early + callback(data.token) + else + vim.notify('go.nvim [AI]: unexpected Copilot token response', vim.log.levels.ERROR) + end + end) + end) end --- Generic helper: POST a chat completion request via curl @@ -458,10 +450,7 @@ local function send_openai_raw(sys_prompt, user_msg, opts, callback) local model = cfg.model or 'gpt-4o-mini' if not api_key or api_key == '' then - vim.notify( - 'go.nvim [AI]: API key not found. Set the ' .. env_name .. ' environment variable', - vim.log.levels.ERROR - ) + vim.notify('go.nvim [AI]: API key not found. Set the ' .. env_name .. ' environment variable', vim.log.levels.ERROR) return end @@ -477,7 +466,10 @@ end function M.run(opts) local cfg = _GO_NVIM_CFG.ai or {} if not cfg.enable then - vim.notify('go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoAI', vim.log.levels.WARN) + vim.notify( + 'go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoAI', + vim.log.levels.WARN + ) return end @@ -532,8 +524,7 @@ end -- ─── GoCodeReview ──────────────────────────────────────────────────────────── ---- Detect the default branch of the current git repo (main or master). ---- Falls back to 'main' if neither is found. +--- Detect the default branch ofnd. --- @return string local function detect_default_branch() -- Check remote HEAD first (most reliable) @@ -560,27 +551,24 @@ end --- @param callback function Called with (diff_text, err_msg) local function get_git_diff(filepath, branch, callback) local rel = vim.fn.fnamemodify(filepath, ':.') - vim.system( - { 'git', 'diff', branch .. '...HEAD', '--', rel }, - { text = true }, - function(result) - vim.schedule(function() - if result.code ~= 0 then - callback(nil, 'git diff failed: ' .. (result.stderr or ''):gsub('%s+$', '')) - return - end - local diff = vim.trim(result.stdout or '') - if diff == '' then - callback(nil, 'no changes against ' .. branch) - return - end - callback(diff, nil) - end) - end - ) + vim.system({ 'git', 'diff', '-U10', branch .. '...HEAD', '--', rel }, { text = true }, function(result) + vim.schedule(function() + if result.code ~= 0 then + callback(nil, 'git diff failed: ' .. (result.stderr or ''):gsub('%s+$', '')) + return + end + local diff = vim.trim(result.stdout or '') + if diff == '' then + callback(nil, 'no changes against ' .. branch) + return + end + callback(diff, nil) + end) + end) end -local code_review_system_prompt = [[You are an experienced Golang code reviewer. Your task is to review Go language source code for correctness, readability, performance, best practices, and style. Carefully analyze the given Go code snippet or file and provide specific actionable feedback to improve quality. Identify issues such as bugs, inefficient constructs, poor naming, inconsistent formatting, concurrency pitfalls, error handling mistakes, or deviations from idiomatic Go. Suggest precise code changes and explain why they improve the code. +local code_review_system_prompt = + [[You are an experienced Golang code reviewer. Your task is to review Go language source code for correctness, readability, performance, best practices, and style. Carefully analyze the given Go code snippet or file and provide specific actionable feedback to improve quality. Identify issues such as bugs, inefficient constructs, poor naming, inconsistent formatting, concurrency pitfalls, error handling mistakes, or deviations from idiomatic Go. Suggest precise code changes and explain why they improve the code. When reviewing, reason step-by-step about each aspect of the code before concluding. Be polite, professional, and constructive. @@ -597,15 +585,79 @@ When reviewing, reason step-by-step about each aspect of the code before conclud 9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. 10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. +# Go-Specific Review Dimensions + +## Formatting & Naming (Effective Go / Google Style) +- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). +- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. +- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). +- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. +- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. +- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. + +## Initialization & Control (The "Go Way") +- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. +- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). +- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). +- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). +- Prefer := for non-zero initialization, var for zero-value declarations. +- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. + +## Data Integrity & Memory (100 Go Mistakes) +- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. +- Conversions: Ensure string-to-slice conversions are necessary and efficient. +- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. +- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. +- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. +- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). + +## Concurrency & Errors +- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). +- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. +- Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. +- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. +- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. + +## Documentation & API Design (Google Style) +- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. +- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). +- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. +- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. +- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. + +## Testing (Google Style) +- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. +- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). +- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. +- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. +- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). + +## Global State & Dependencies +- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. +- Flag service locator patterns and thick-client singletons. + +## String Handling (Google Style) +- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. +- Use backticks for constant multi-line strings. + +# Output Instructions + +For every critique, provide: +1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). +2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. +3. A brief refactored code suggestion where applicable. + # Instructions 1. Read the entire Go code provided. 2. For each audit category above, check whether any issues apply. -3. Assess functionality and correctness. -4. Evaluate code readability and style against Go conventions. -5. Check for performance or concurrency issues. -6. Review error handling and package usage. -7. Provide only actionable improvements β€” skip praise or explanations of what is already good. +3. For each Go-specific review dimension, check whether any issues apply. +4. Assess functionality and correctness. +5. Evaluate code readability and style against Go conventions. +6. Check for performance or concurrency issues. +7. Review error handling and package usage. +8. Provide only actionable improvements β€” skip praise or explanations of what is already good. # Output Format @@ -620,21 +672,19 @@ If there are NO improvements needed: - Output exactly one line: a brief overall summary (e.g. "Code looks idiomatic and correct."). If there ARE improvements, output ONLY lines in vim quickfix format: - ::: : + ::: : [] : . Refactor: where is: error β€” compile errors and logic errors only (code will not build or produces wrong results) warning β€” issues that must be handled for production: memory leaks, heap escapes, missing/incorrect timeouts, unclosed resources, unhandled signals, etc. - suggestion β€” all other improvements: style, naming, readability, idiomatic Go, minor refactors, etc. + info β€” all other improvements: style, naming, readability, idiomatic Go, minor refactors, etc. +and is one of: Effective Go, 100 Go Mistakes, Google Style Example input: L41| params := map[string]string{ L42| "ProjectID": projectID, - L43| "ServingConfig": os.Getenv("GCP_SEARCH_SERVING_CONFIG"), + L43| "ServingConfig": os.Getenv("SERVING_CONFIG"), L44| } -Example output (issue is on L43 where os.Getenv is called): - main.go:43:20: warning: os.Getenv called inline; consider reading env var at startup and validating it - CRITICAL: Read the "L|" prefix of the EXACT line containing the issue. That number is the line number you must use. Do NOT use the line number of a nearby or enclosing line. Rules: @@ -647,7 +697,8 @@ Rules: If code is not provided, output exactly: error: no Go source code provided for review. ]] -local diff_review_system_prompt = [[You are an experienced Golang code reviewer. You are reviewing a unified diff (git diff) of Go source code changes against a base branch. Focus ONLY on the changed lines (lines starting with + or context around them). Evaluate the changes for correctness, readability, performance, best practices, and style. +local diff_review_system_prompt = + [[You are an experienced Golang code reviewer. You are reviewing a unified diff (git diff) of Go source code changes against a base branch. Focus ONLY on the changed lines (lines starting with + or context around them). Evaluate the changes for correctness, readability, performance, best practices, and style. IMPORTANT: Use the NEW file line numbers from the diff hunk headers (the second number in @@ -a,b +c,d @@). For added/changed lines (starting with +), compute the actual file line number by counting from the hunk start. @@ -664,13 +715,77 @@ IMPORTANT: Use the NEW file line numbers from the diff hunk headers (the second 9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. 10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. +# Go-Specific Review Dimensions + +## Formatting & Naming (Effective Go / Google Style) +- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). +- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. +- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). +- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. +- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. +- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. + +## Initialization & Control (The "Go Way") +- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. +- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). +- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). +- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). +- Prefer := for non-zero initialization, var for zero-value declarations. +- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. + +## Data Integrity & Memory (100 Go Mistakes) +- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. +- Conversions: Ensure string-to-slice conversions are necessary and efficient. +- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. +- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. +- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. +- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). + +## Concurrency & Errors +- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). +- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. +- Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. +- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. +- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. + +## Documentation & API Design (Google Style) +- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. +- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). +- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. +- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. +- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. + +## Testing (Google Style) +- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. +- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). +- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. +- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. +- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). + +## Global State & Dependencies +- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. +- Flag service locator patterns and thick-client singletons. + +## String Handling (Google Style) +- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. +- Use backticks for constant multi-line strings. + +# Output Instructions + +For every critique, provide: +1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). +2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. +3. A brief refactored code suggestion where applicable. + # Instructions 1. Read the unified diff carefully. 2. Focus only on the added/modified code (+ lines). 3. For each audit category above, check whether any issues apply to the changed code. -4. Evaluate changed code for bugs, style, performance, concurrency, error handling. -5. Skip praise β€” output improvements only. +4. For each Go-specific review dimension, check whether any issues apply to the changed code. +5. Evaluate changed code for bugs, style, performance, concurrency, error handling. +6. Skip praise β€” output improvements only. # Output Format @@ -678,15 +793,15 @@ If there are NO improvements needed: - Output exactly one line: a brief summary (e.g. "Changes look correct and idiomatic."). If there ARE improvements, output ONLY lines in vim quickfix format: - ::: : + ::: : [] : . Refactor: where is: error β€” compile errors and logic errors only (code will not build or produces wrong results) warning β€” issues that must be handled for production: memory leaks, heap escapes, missing/incorrect timeouts, unclosed resources, unhandled signals, etc. - suggestion β€” all other improvements: style, naming, readability, idiomatic Go, minor refactors, etc. + info β€” all other improvements: style, naming, readability, idiomatic Go, minor refactors, etc. +and is one of: Effective Go, 100 Go Mistakes, Google Style Rules: -- Do NOT output any introduction, summary header, markdown, or conclusion. -- Do NOT use code blocks or bullet points. +- Do NOT output any introduction, summary header, markdown, or bullet points. - Each issue must be a separate line in the exact quickfix format above. - Line numbers must be the NEW file line numbers (post-change), 1-based. - Focus on practical, specific improvements only. @@ -776,7 +891,10 @@ end function M.code_review(opts) local cfg = _GO_NVIM_CFG.ai or {} if not cfg.enable then - vim.notify('go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoCodeReview', vim.log.levels.WARN) + vim.notify( + 'go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoCodeReview', + vim.log.levels.WARN + ) return end @@ -806,10 +924,7 @@ function M.code_review(opts) return end local short_name = vim.fn.expand('%:t') - local user_msg = string.format( - 'File: %s\nBase branch: %s\n\n```diff\n%s\n```', - short_name, branch, diff - ) + local user_msg = string.format('File: %s\nBase branch: %s\n\n```diff\n%s\n```', short_name, branch, diff) M.request(diff_review_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) handle_review_response(resp, filename) end) @@ -848,10 +963,156 @@ function M.code_review(opts) end) end --- ─── Public request helper ──────────────────────────────────────────────────── +-- --------------------------------------------------------------------------- +-- GoAIChat +-- --------------------------------------------------------------------------- + +local chat_system_prompt = [[You are an expert Go developer and code assistant embedded in Neovim via go.nvim. +The user may ask you to explain, examine, refactor, check, or otherwise discuss Go code or general Go questions. + +Guidelines: +- Be concise but thorough. Prefer short paragraphs over long walls of text. +- When showing code, use plain fenced Go blocks (no extra commentary outside the block unless needed). +- When refactoring, show only the changed/relevant portion, not the entire file. +- When explaining, prefer bullet points for lists of properties or steps. +- Get straight to the answer. +- If the user provides a code snippet, treat it as the subject of the question. +- Always assume Go unless the user says otherwise. +]] + +--- Render the chat response in a floating scratch window +--- @param response string +--- @param title string|nil +local function open_chat_float(response, title) + local lines = vim.split(response, '\n', { plain = true }) + + -- Add a blank leading line for padding + table.insert(lines, 1, '') + table.insert(lines, '') + + local width = math.min(math.max(60, vim.o.columns - 20), 120) + local height = math.min(#lines + 2, math.floor(vim.o.lines * 0.7)) + + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) + vim.api.nvim_set_option_value('buftype', 'nofile', { buf = buf }) + vim.api.nvim_set_option_value('filetype', 'markdown', { buf = buf }) + + local row = math.floor((vim.o.lines - height) / 2) + local col = math.floor((vim.o.columns - width) / 2) + + local win = vim.api.nvim_open_win(buf, true, { + relative = 'editor', + width = width, + height = height, + row = row, + col = col, + style = 'minimal', + border = 'rounded', + title = ' ' .. (title or 'GoAIChat') .. ' ', + title_pos = 'center', + }) + vim.api.nvim_set_option_value('wrap', true, { win = win }) + vim.api.nvim_set_option_value('linebreak', true, { win = win }) + + -- Close keymaps + for _, key in ipairs({ 'q', '', '' }) do + vim.keymap.set('n', key, function() + if vim.api.nvim_win_is_valid(win) then + vim.api.nvim_win_close(win, true) + end + end, { buffer = buf, nowait = true, silent = true }) + end +end + +--- Build a user message for GoAIChat, optionally embedding a code snippet +--- @param question string +--- @param code string|nil selected or surrounding code, may be nil +--- @param lang string|nil filetype / language hint +--- @return string +local function build_chat_user_msg(question, code, lang) + lang = lang or 'go' + if code and code ~= '' then + return string.format('%s\n\n```%s\n%s\n```', question, lang, code) + end + return question +end + +--- Entry point for :GoAIChat +--- @param opts table Standard nvim command opts +function M.chat(opts) + local cfg = _GO_NVIM_CFG.ai or {} + if not cfg.enable then + vim.notify( + 'go.nvim [AI]: AI is disabled. Set ai = { enable = true } in go.nvim setup to use GoAIChat', + vim.log.levels.WARN + ) + return + end + + local fargs = (type(opts) == 'table' and opts.fargs) or {} + local question = vim.trim(table.concat(fargs, ' ')) + + -- Collect visual selection or surrounding function context + local code = nil + local lang = vim.bo.filetype or 'go' + + if type(opts) == 'table' and opts.range and opts.range == 2 then + -- Visual selection + local sel_lines = vim.api.nvim_buf_get_lines(0, opts.line1 - 1, opts.line2, false) + code = table.concat(sel_lines, '\n') + end + + local diff_text + -- create git comments require git diff context + if string.find(question, 'create a commit summary') then + -- get diff against master/main + local branch = 'master' + if string.find(question, 'main') then + branch = 'main' + end + + local code_text = vim.fn.system({ 'git', 'diff', '-U10', branch, '--', '*.go' }) + + if not code_text or #code_text == 0 then + vim.notify('No code to commit', vim.log.levels.WARN) + return + end + diff_text = code_text + end + + local function dispatch(q) + if q == '' then + vim.notify('[GoAIChat]: empty question', vim.log.levels.WARN) + return + end + local user_msg = build_chat_user_msg(q, code, lang) + vim.notify('[GoAIChat]: thinking …', vim.log.levels.INFO) + M.request(chat_system_prompt, user_msg, { max_tokens = 2000, temperature = 0.2 }, function(resp) + open_chat_float(resp, q:sub(1, 60)) + end) + end + + if diff_text then + return dispatch(diff_text) + end + if question ~= '' then + dispatch(question) + else + -- Interactive prompt + vim.ui.input({ + prompt = 'GoAIChat> ', + default = code and 'explain this code' or '', + }, function(input) + if input and input ~= '' then + dispatch(input) + end + end) + end +end ---- @param sys_prompt string The system prompt ---- @param user_msg string The user message +-- ─── Public request helper ───────── --- @param opts table|nil Optional: { temperature, max_tokens } --- @param callback function Called with the response text string function M.request(sys_prompt, user_msg, opts, callback) @@ -874,5 +1135,6 @@ end M.code_review_system_prompt = code_review_system_prompt M.diff_review_system_prompt = diff_review_system_prompt +M.chat_system_prompt = chat_system_prompt return M diff --git a/lua/go/commands.lua b/lua/go/commands.lua index 76c428c86..3c8288e1e 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -656,53 +656,52 @@ return { end, { nargs = '*', range = true }) ---@param args table the opts table passed by nvim_create_user_command ----@return table parsed options -local function parse_review_args(args) - local opts = {} - local fargs = args.fargs or {} - - -- Check for visual range selection - if args.range and args.range == 2 then - opts.visual = true - local start_line = args.line1 - local end_line = args.line2 - local bufnr = vim.api.nvim_get_current_buf() - local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false) - opts.lines = table.concat(lines, '\n') - opts.start_line = start_line - opts.end_line = end_line - end + ---@return table parsed options + local function parse_review_args(args) + local opts = {} + local fargs = args.fargs or {} + + -- Check for visual range selection + if args.range and args.range == 2 then + opts.visual = true + local start_line = args.line1 + local end_line = args.line2 + local bufnr = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(bufnr, start_line - 1, end_line, false) + opts.lines = table.concat(lines, '\n') + opts.start_line = start_line + opts.end_line = end_line + end - for i, arg in ipairs(fargs) do - if arg == '-d' or arg == '--diff' then - opts.diff = true - -- Next arg might be branch name - if fargs[i + 1] and not fargs[i + 1]:match('^%-') then - opts.branch = fargs[i + 1] + for i, arg in ipairs(fargs) do + if arg == '-d' or arg == '--diff' then + opts.diff = true + -- Next arg might be branch name + if fargs[i + 1] and not fargs[i + 1]:match('^%-') then + opts.branch = fargs[i + 1] + end + end end - end - end - -- Default branch if diff mode but no branch specified - if opts.diff and not opts.branch then - opts.branch = 'master' - end + -- Default branch if diff mode but no branch specified + if opts.diff and not opts.branch then + opts.branch = 'master' + end - return opts -end + return opts + end create_cmd('GoCodeReview', function(args) + local opts = parse_review_args(args) - local opts = parse_review_args(args) - - -- Use MCP-enhanced review when available - local go_cfg = require('go').config() or {} - if go_cfg.mcp and go_cfg.mcp.enable then + -- Use MCP-enhanced review when available + local go_cfg = require('go').config() or {} + if go_cfg.mcp and go_cfg.mcp.enable then require('go.mcp.review').review(opts) - else + else -- Fallback to existing review without MCP require('go.ai.review').review(opts) - end + end end, { nargs = '*', range = true, @@ -719,5 +718,25 @@ end return package.loaded.go.doc_complete(a, l) end, }) + create_cmd('GoAIChat', function(opts) + require('go.ai').chat(opts) + end, { + nargs = '*', + range = true, + complete = function(_, _, _) + return { + 'explain this code', + 'refactor this code', + 'check for bugs', + 'examine error handling', + 'simplify this', + 'what does this do', + 'suggest improvements', + 'check concurrency safety', + 'create a commit summary', + 'convert to idiomatic Go', + } + end, + }) end, } diff --git a/lua/go/mcp/client.lua b/lua/go/mcp/client.lua index 8b94c4968..a0c225d5d 100644 --- a/lua/go/mcp/client.lua +++ b/lua/go/mcp/client.lua @@ -4,9 +4,6 @@ local log = require('go.utils').log local json = vim.json ---@class McpClient ----@field handle uv_process_t ----@field stdin uv_pipe_t ----@field stdout uv_pipe_t ---@field pending table ---@field next_id number ---@field buffer string @@ -159,4 +156,4 @@ function McpClient:shutdown() end end -return M \ No newline at end of file +return M diff --git a/lua/go/mcp/context.lua b/lua/go/mcp/context.lua index 484829fc7..004528d5f 100644 --- a/lua/go/mcp/context.lua +++ b/lua/go/mcp/context.lua @@ -87,14 +87,14 @@ local function format_ref_location(uri, line) local lnum = line + 1 if is_test_file(fname) then - return string.format(' %s:%d', fname, lnum) + return string.format('\t- %s:%d', fname, lnum) end local text = read_line(fpath, lnum) if text then - return string.format(' %s:%d `%s`', fname, lnum, text) + return string.format('\t- %s:%d `%s`', fname, lnum, text) end - return string.format(' %s:%d', fname, lnum) + return string.format('\t- %s:%d', fname, lnum) end --- Format a caller location, including line text for non-test files @@ -108,14 +108,14 @@ local function format_caller_location(uri, line, caller_name) local lnum = line + 1 if is_test_file(fname) then - return string.format(' %s:%d β€” %s()', fname, lnum, caller_name) + return string.format('\t- %s:%d β€” %s()', fname, lnum, caller_name) end local text = read_line(fpath, lnum) if text then - return string.format(' %s:%d β€” %s() `%s`', fname, lnum, caller_name, text) + return string.format('\t- %s:%d β€” %s() `%s`', fname, lnum, caller_name, text) end - return string.format(' %s:%d β€” %s()', fname, lnum, caller_name) + return string.format('\t- %s:%d β€” %s()', fname, lnum, caller_name) end --- Use treesitter to find symbols (functions, types, methods) at given lines @@ -201,17 +201,17 @@ function M.get_symbol_context_via_lsp(bufnr, line, col, callback) local seen = {} for _, ref in ipairs(result) do local fname = vim.fn.fnamemodify(vim.uri_to_fname(ref.uri), ':.') - local key = string.format('%s:%d', fname, ref.range.start.line + 1) + local key = string.format('- %s:%d', fname, ref.range.start.line + 1) if not seen[key] then seen[key] = true table.insert(refs, format_ref_location(ref.uri, ref.range.start.line)) end - if #refs >= 20 then - table.insert(refs, string.format(' ... and %d more', #result - 20)) + if #refs >= 10 then + table.insert(refs, string.format(' ... and %d more', #result - 10)) break end end - table.insert(results, '### References (' .. #result .. '):\n' .. table.concat(refs, '\n')) + table.insert(results, '\n* References (' .. #result .. '):\n' .. table.concat(refs, '\n')) end check_done() end) @@ -234,7 +234,7 @@ function M.get_symbol_context_via_lsp(bufnr, line, col, callback) break end end - table.insert(results, '### Callers (' .. #calls .. '):\n' .. table.concat(callers, '\n')) + table.insert(results, '\n* Callers (' .. #calls .. '):\n' .. table.concat(callers, '\n')) end check_done() end) @@ -250,7 +250,7 @@ function M.get_symbol_context_via_lsp(bufnr, line, col, callback) break end end - table.insert(results, '### Implementations (' .. #result .. '):\n' .. table.concat(impls, '\n')) + table.insert(results, '\n* Implementations (' .. #result .. '):\n' .. table.concat(impls, '\n')) end check_done() end) @@ -308,7 +308,8 @@ function M.gather_diff_context(diff_text, callback) local symbol_list = vim.tbl_values(symbols) if #symbol_list == 0 then - table.insert(all_context, string.format('## File: %s\n(changed lines do not contain function/type declarations)', file)) + table.insert(all_context, + string.format('## File: %s\n(changed lines do not contain function/type declarations)', file)) files_pending = files_pending - 1 if files_pending == 0 then callback(table.concat(all_context, '\n\n')) @@ -316,7 +317,7 @@ function M.gather_diff_context(diff_text, callback) else local syms_pending = #symbol_list for _, sym in ipairs(symbol_list) do - local header = string.format('## Symbol: `%s` (%s) in %s:%d', sym.name, sym.kind, file, sym.line + 1) + local header = string.format('### Symbol: `%s` (%s) in %s:%d', sym.name, sym.kind, file, sym.line + 1) M.get_symbol_context_via_lsp(bufnr, sym.line, sym.col, function(ctx) if ctx and #ctx > 0 then @@ -366,7 +367,7 @@ function M.gather_buffer_context(bufnr, callback) local pending = #symbols for _, sym in ipairs(symbols) do - local header = string.format('## Symbol: `%s` (%s) in %s:%d', sym.name, sym.kind, file, sym.line + 1) + local header = string.format('* Symbol: `%s` (%s) in %s:%d', sym.name, sym.kind, file, sym.line + 1) M.get_symbol_context_via_lsp(bufnr, sym.line, sym.col, function(ctx) if ctx and #ctx > 0 then @@ -383,4 +384,5 @@ function M.gather_buffer_context(bufnr, callback) end end -return M \ No newline at end of file +return M + diff --git a/lua/go/mcp/init.lua b/lua/go/mcp/init.lua index 4f2ab5dbe..0efbbc970 100644 --- a/lua/go/mcp/init.lua +++ b/lua/go/mcp/init.lua @@ -1,4 +1,4 @@ -local M = {_client = nil} +local M = { _client = nil } local client_mod = require('go.mcp.client') function M.setup(opts) @@ -23,5 +23,4 @@ function M.shutdown() end end - -return M \ No newline at end of file +return M diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index b4b097226..91508d8cc 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -8,8 +8,9 @@ local log = require('go.utils').log ---@param opts table ---@return string the full prompt for AI local function build_enriched_prompt(diff_text, semantic_context, opts) - return string.format([[ -You are an expert Go code reviewer. Review the following code changes. + return string.format( + [[ +You are an experienced Golang code reviewer with access to MCP tools. Your task is to review Go language source code for correctness, readability, performance, best practices, and style using all available context. You have access to **semantic context** gathered from gopls β€” this includes information about callers, references, and interface implementations for @@ -27,17 +28,111 @@ every changed symbol. Use this to assess the **full impact** of the changes: ## Semantic Context for Changed Symbols %s -## Instructions -1. Identify bugs, race conditions, security issues, and API contract violations. -2. Flag changes that break callers or interface implementations (you have the data). -3. Check error handling, nil safety, and resource cleanup. -4. Assess backward compatibility using the reference/caller information. -5. Suggest concrete improvements. - -Respond ONLY with a JSON array. Each element: -{"file":"","line":,"severity":"error|warning|info","message":""} -No markdown wrapping. Just the JSON array. -]], diff_text, semantic_context) +When reviewing, reason step-by-step about each aspect of the code before concluding. Be polite, professional, and constructive. + +# Audit Categories + +1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. +2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. +3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. +4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. +5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. +6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %%w. +7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. +8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. +9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. +10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. + +# Go-Specific Review Dimensions + +## Formatting & Naming (Effective Go / Google Style) +- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). +- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. +- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). +- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. +- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. +- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. + +## Initialization & Control (The "Go Way") +- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. +- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). +- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). +- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). +- Prefer := for non-zero initialization, var for zero-value declarations. +- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. + +## Data Integrity & Memory (100 Go Mistakes) +- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. +- Conversions: Ensure string-to-slice conversions are necessary and efficient. +- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. +- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. +- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. +- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). + +## Concurrency & Errors +- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). +- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. +- Error Wrapping: Ensure %%w is used (not %%v) when callers need to inspect wrapped errors. Place %%w at the end of the format string. Avoid redundant annotations (e.g., "failed: %%v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. +- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. +- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. + +## Documentation & API Design (Google Style) +- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. +- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). +- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. +- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. +- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. + +## Testing (Google Style) +- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. +- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). +- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. +- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. +- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). + +## Global State & Dependencies +- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. +- Flag service locator patterns and thick-client singletons. + +## String Handling (Google Style) +- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. +- Use backticks for constant multi-line strings. + +# Output Instructions + +For every critique, provide: +1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). +2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. +3. A brief refactored code suggestion where applicable. + +# Instructions + +1. Read the entire Go code provided. +2. Use MCP tools to gather additional context if available (e.g., read related files, check project structure). +3. For each audit category above, check whether any issues apply. +4. For each Go-specific review dimension, check whether any issues apply. +5. Assess functionality and correctness. +6. Evaluate code readability and style against Go conventions. +7. Check for performance or concurrency issues. +8. Review error handling and package usage. +9. Provide only actionable improvements β€” skip praise or explanations of what is already good. + +Output as JSON array of objects with fields: +- "file": filename +- "line": line number (integer) +- "col": column number (integer, default 1) +- "severity": "error" | "warning" | "info" +- "violation": short violation label (e.g., "Non-idiomatic naming", "Slice memory leak") +- "principle": one of "[Effective Go]", "[100 Go Mistakes]", "[Google Style]" +- "message": description of the issue +- "refactor": brief refactored code or suggestion (optional, empty string if not applicable) + +If no issues found, return: [] +]], + diff_text, + semantic_context + ) end --- Enhanced code review with semantic context from the running gopls LSP @@ -49,11 +144,10 @@ function M.review(opts) -- Get the diff or file content (reuse existing logic) local code_text if opts.diff then - local branch = opts.branch or 'main' - code_text = vim.fn.system({ 'git', 'diff', branch, '--', '*.go' }) + local branch = opts.branch or 'master' + code_text = vim.fn.system({ 'git', 'diff', '-U10', branch, '--', '*.go' }) if vim.v.shell_error ~= 0 then - -- Try master if main doesn't exist - code_text = vim.fn.system({ 'git', 'diff', 'master', '--', '*.go' }) + code_text = vim.fn.system({ 'git', 'diff', '-U10', 'main', '--', '*.go' }) end elseif opts.visual and opts.lines then code_text = opts.lines @@ -67,18 +161,13 @@ function M.review(opts) return end - vim.notify('go.nvim [Review]: gathering semantic context from gopls...', vim.log.levels.INFO) + vim.notify('[GoReview]: gathering semantic context from gopls...', vim.log.levels.INFO) local function send_review(sys_prompt, semantic_ctx) local prompt = build_enriched_prompt(code_text, semantic_ctx, opts) - ai.request( - sys_prompt, - prompt, - { max_tokens = 4096, temperature = 0 }, - function(response) - M._handle_review_response(response) - end - ) + ai.request(sys_prompt, prompt, { max_tokens = 4096, temperature = 0 }, function(response) + M._handle_review_response(response) + end) end if opts.diff then @@ -119,18 +208,36 @@ function M._handle_review_response(response) end local qf_items = {} - for _, f in ipairs(findings) do + log(findings) + for _, item in ipairs(findings) do local severity_map = { error = 'E', warning = 'W', info = 'I', } + -- Build enriched message with violation/principle/refactor when available + + local parts = {} + if item.violation and item.violation ~= '' then + table.insert(parts, '[' .. item.violation .. ']') + end + if item.principle and item.principle ~= '' then + table.insert(parts, item.principle) + end + if item.message and item.message ~= '' then + table.insert(parts, item.message) + end + if item.refactor and item.refactor ~= '' then + table.insert(parts, 'Refactor: ' .. item.refactor) + end + local text = table.concat(parts, ' ') + table.insert(qf_items, { - filename = f.file or vim.fn.expand('%'), - lnum = f.line or 1, - col = 1, - text = f.message or '', - type = severity_map[f.severity] or 'I', + filename = item.file or vim.fn.expand('%'), + lnum = tonumber(item.line) or 1, + col = tonumber(item.col) or 1, + text = text, + type = severity_map[item.severity] or 'W', }) end @@ -143,4 +250,4 @@ function M._handle_review_response(response) end) end -return M \ No newline at end of file +return M From cdbde1c5a5fd4a4f0e3be46efdba5a502efac554 Mon Sep 17 00:00:00 2001 From: ray-x Date: Sun, 8 Mar 2026 11:39:12 +1100 Subject: [PATCH 08/21] add short version of prompts --- README.md | 4 +- doc/go.txt | 21 ++++++++-- lua/go/ai.lua | 91 ++++++++++++++++++++++++++++++++++++------- lua/go/commands.lua | 29 +++++++++++++- lua/go/gotest.lua | 11 ++---- lua/go/mcp/client.lua | 2 +- lua/go/mcp/review.lua | 61 ++++++++++++++++++++++------- 7 files changed, 175 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index c6426777c..c43812634 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ The plugin covers most features required for a gopher. **AI-Powered** -- `GoAI` β€” natural-language command dispatcher (translates natural-language into go.nvim commands via Copilot/OpenAI) +- `GoAI` β€” natural-language command dispatcher (translates natural-language into go.nvim commands via Copilot/OpenAI). Use `-f` to include the full command catalog in the prompt for better accuracy - `GoCmtAI` β€” generate doc comments with AI for the declaration at cursor - `GoDocAI` β€” AI-powered documentation: find symbols by vague name and generate rich docs from source - `GoCodeReview` β€” AI code review for files, selections, or diffs; results populate the quickfix list @@ -782,6 +782,8 @@ actionable findings (errors, warnings, suggestions). | :'<,'>GoCodeReview | Review the visual selection only | | GoCodeReview -d | Review only changes (diff) against the default branch (main/master) | | GoCodeReview -d develop | Review only changes (diff) against a specific branch | +| GoCodeReview -b | Review with a brief/compact prompt (saves tokens) | +| GoCodeReview -d -b | Diff review with brief prompt | Requires `ai = { enable = true }` in your go.nvim setup. Results are loaded into the quickfix list. diff --git a/doc/go.txt b/doc/go.txt index 305b94782..1a9e8fc14 100644 --- a/doc/go.txt +++ b/doc/go.txt @@ -543,24 +543,38 @@ GOPLS LSP COMMANDS ~ AI-POWERED ~ -:GoAI {natural_language_request} *:GoAI* +:GoAI [-f] {natural_language_request} *:GoAI* Translate a natural language request into the correct go.nvim command using an LLM (Copilot or OpenAI-compatible). Supports visual range β€” range is forwarded to range-capable commands. If no argument is given, opens an interactive prompt. + + Flags: + -f Include the full command catalog in the + prompt for better accuracy (uses more + tokens). + Examples: > :GoAI run unit test for tags test - :GoAI format file with gofumpt + :GoAI -f format file with gofumpt :GoAI add json tags to struct :'<,'>GoAI convert this json to a go struct < Configure via the |ai| option. -:GoCodeReview [-d [{branch}]] *:GoCodeReview* +:GoCodeReview [-d [{branch}]] [-b] *:GoCodeReview* Review Go code with AI and populate the quickfix list with actionable findings (errors, warnings, suggestions). Uses the configured AI provider (see |ai| option). + Flags: + -d [{branch}] Review only changes (diff) against a + branch. If no branch is given, the default + branch (main/master) is auto-detected. + -b, --brief Use a compact prompt that focuses on bugs, + correctness, and major issues (saves + tokens). + Modes: :GoCodeReview Review the entire current file. :'<,'>GoCodeReview Review the visual selection only. @@ -570,6 +584,7 @@ AI-POWERED ~ :GoCodeReview -d develop Review changes against a specific branch. + :GoCodeReview -b Review with brief/compact prompt. Findings are parsed into vim quickfix format with severity: error (E), warning (W), or suggestion (I). diff --git a/lua/go/ai.lua b/lua/go/ai.lua index 5ced591fc..ee80b2592 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -182,7 +182,7 @@ AI-POWERED: - GoDocAI [query] β€” Find a function/type by vague name and generate rich AI documentation from its source code ]] -local system_prompt = [[You are a command translator for go.nvim, a Neovim plugin for Go development. +local system_prompt_base = [[You are a command translator for go.nvim, a Neovim plugin for Go development. Your job is to translate natural language requests into the correct go.nvim Vim command. Rules: @@ -192,7 +192,9 @@ Rules: 4. If the request is ambiguous, choose the most likely command. 5. If the request cannot be mapped to any go.nvim command, return exactly: echo "No matching go.nvim command found" -]] .. command_catalog +]] + +local system_prompt = system_prompt_base .. command_catalog --- Read Copilot OAuth token from the config files written by copilot.vim / copilot.lua local function get_copilot_oauth_token() @@ -476,8 +478,20 @@ function M.run(opts) local prompt local range_prefix = '' + local full_catalog = false + if type(opts) == 'table' then - prompt = table.concat(opts.fargs or {}, ' ') + local fargs = opts.fargs or {} + -- Check for -f flag to include full command catalog + local filtered = {} + for _, arg in ipairs(fargs) do + if arg == '-f' then + full_catalog = true + else + table.insert(filtered, arg) + end + end + prompt = table.concat(filtered, ' ') -- Capture visual range if the command was called with one if opts.range and opts.range == 2 then range_prefix = string.format('%d,%d', opts.line1, opts.line2) @@ -490,23 +504,24 @@ function M.run(opts) if prompt == '' then vim.ui.input({ prompt = 'go.nvim AI> ' }, function(input) if input and input ~= '' then - M._dispatch(input, range_prefix) + M._dispatch(input, range_prefix, full_catalog) end end) return end - M._dispatch(prompt, range_prefix) + M._dispatch(prompt, range_prefix, full_catalog) end --- Dispatch the natural language request to the configured LLM provider -function M._dispatch(prompt, range_prefix) +function M._dispatch(prompt, range_prefix, full_catalog) local cfg = _GO_NVIM_CFG.ai or {} local provider = cfg.provider or 'copilot' local confirm = cfg.confirm ~= false -- default true vim.notify('go.nvim [AI]: thinking …', vim.log.levels.INFO) + local sys_prompt = full_catalog and system_prompt or system_prompt_base local user_msg = build_user_message(prompt) local function on_resp(resp) @@ -514,9 +529,9 @@ function M._dispatch(prompt, range_prefix) end if provider == 'copilot' then - send_copilot_raw(system_prompt, user_msg, {}, on_resp) + send_copilot_raw(sys_prompt, user_msg, {}, on_resp) elseif provider == 'openai' then - send_openai_raw(system_prompt, user_msg, {}, on_resp) + send_openai_raw(sys_prompt, user_msg, {}, on_resp) else vim.notify('go.nvim [AI]: unknown provider "' .. provider .. '"', vim.log.levels.ERROR) end @@ -632,6 +647,9 @@ When reviewing, reason step-by-step about each aspect of the code before conclud - t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. - Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. - Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). +- Mocking: For external dependencies, prefer interface-based design that allows manual mocks. Flag overuse of complex mocking frameworks that generate code or require heavy setup. +- Logging: Avoid log output in tests except for debugging. Use t.Log for test logs, which are only shown on failure or with verbose flag. +- Avoid shared state between tests. Each test should be independent and repeatable. ## Global State & Dependencies - Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. @@ -697,6 +715,37 @@ Rules: If code is not provided, output exactly: error: no Go source code provided for review. ]] +local code_review_system_prompt_short = + [[You are an experienced Go code reviewer. Review the given Go code for bugs, correctness, error handling, concurrency issues, and major style problems. Focus on actionable issues only. + +The source code has line markers "L|" at the start of each line. Use those numbers for line references. + +If there are NO issues: output one summary line (e.g. "Code looks correct and idiomatic."). + +If there ARE issues, output ONLY lines in vim quickfix format: + ::: : . Refactor: +where is: error (bugs/compile errors), warning (resource leaks, races, missing cleanup), info (style/naming). + +Rules: +- No markdown, no headers, no bullet points. One quickfix line per issue. +- Line numbers MUST match the L prefixes in the provided code. +]] + +local diff_review_system_prompt_short = + [[You are an experienced Go code reviewer. Review the unified diff for bugs, correctness, error handling, concurrency issues, and major style problems in the changed lines (+ lines) only. + +Use NEW file line numbers from diff hunk headers (@@ -a,b +c,d @@). + +If there are NO issues: output one summary line. + +If there ARE issues, output ONLY lines in vim quickfix format: + ::: : . Refactor: +where is: error, warning, info. + +Rules: +- No markdown, no headers, no bullet points. One quickfix line per issue. +]] + local diff_review_system_prompt = [[You are an experienced Golang code reviewer. You are reviewing a unified diff (git diff) of Go source code changes against a base branch. Focus ONLY on the changed lines (lines starting with + or context around them). Evaluate the changes for correctness, readability, performance, best practices, and style. @@ -762,6 +811,8 @@ IMPORTANT: Use the NEW file line numbers from the diff hunk headers (the second - t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. - Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. - Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). +- Mocking: Prefer interfaces for dependencies to allow mocking. Flag untestable code with hard-coded dependencies or side effects. +- Logging: Avoid log package in tests; use t.Log for test output to integrate with test reporting. ## Global State & Dependencies - Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. @@ -900,17 +951,23 @@ function M.code_review(opts) local fargs = (type(opts) == 'table' and opts.fargs) or {} - -- Parse -d [branch] flag + -- Parse flags: -d [branch], -b/--brief local diff_mode = false local diff_branch = nil - for i, arg in ipairs(fargs) do + local brief = false + local i = 1 + while i <= #fargs do + local arg = fargs[i] if arg == '-d' or arg == '--diff' then diff_mode = true if fargs[i + 1] and not fargs[i + 1]:match('^%-') then diff_branch = fargs[i + 1] + i = i + 1 end - break + elseif arg == '-b' or arg == '--brief' then + brief = true end + i = i + 1 end local filename = vim.fn.expand('%:p') @@ -925,7 +982,8 @@ function M.code_review(opts) end local short_name = vim.fn.expand('%:t') local user_msg = string.format('File: %s\nBase branch: %s\n\n```diff\n%s\n```', short_name, branch, diff) - M.request(diff_review_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) + local sys = brief and diff_review_system_prompt_short or diff_review_system_prompt + M.request(sys, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) handle_review_response(resp, filename) end) end) @@ -949,8 +1007,8 @@ function M.code_review(opts) -- Prefix each line with its file line number so the LLM references exact positions local numbered = {} - for i, line in ipairs(lines) do - table.insert(numbered, string.format('L%d| %s', start_line + i - 1, line)) + for i_line, line in ipairs(lines) do + table.insert(numbered, string.format('L%d| %s', start_line + i_line - 1, line)) end local code = table.concat(numbered, '\n') local short_name = vim.fn.expand('%:t') @@ -958,7 +1016,8 @@ function M.code_review(opts) vim.notify('[GoCodeReview]: reviewing …', vim.log.levels.INFO) - M.request(code_review_system_prompt, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) + local sys = brief and code_review_system_prompt_short or code_review_system_prompt + M.request(sys, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) handle_review_response(resp, filename) end) end @@ -1134,7 +1193,9 @@ function M.request(sys_prompt, user_msg, opts, callback) end M.code_review_system_prompt = code_review_system_prompt +M.code_review_system_prompt_short = code_review_system_prompt_short M.diff_review_system_prompt = diff_review_system_prompt +M.diff_review_system_prompt_short = diff_review_system_prompt_short M.chat_system_prompt = chat_system_prompt return M diff --git a/lua/go/commands.lua b/lua/go/commands.lua index 3c8288e1e..64e98c5a2 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -653,7 +653,27 @@ return { create_cmd('GoAI', function(opts) require('go.ai').run(opts) - end, { nargs = '*', range = true }) + end, { nargs = '*', + complete = function(_, _, _) + return { + '-f', -- full command catalog from go.txt + 'test this function', + 'test this file', + 'add tags to this struct', + 'run AI code review', + 'explain this code', + 'refactor this code', + 'check for bugs', + 'examine error handling', + 'simplify this', + 'what does this do', + 'suggest improvements', + 'check concurrency safety', + 'create a commit summary', + 'convert to idiomatic Go', + } + end, + range = true }) ---@param args table the opts table passed by nvim_create_user_command ---@return table parsed options @@ -680,7 +700,12 @@ return { if fargs[i + 1] and not fargs[i + 1]:match('^%-') then opts.branch = fargs[i + 1] end + elseif arg == '-b' or arg == '--brief' then + opts.brief = true + elseif arg == '-f' or arg == '--full' then + opts.full = true end + end -- Default branch if diff mode but no branch specified @@ -706,7 +731,7 @@ return { nargs = '*', range = true, complete = function(_, _, _) - return { '-d', '--diff' } + return { '-d', '--diff', '-b', '--brief', '-f', '--full' } end, }) diff --git a/lua/go/gotest.lua b/lua/go/gotest.lua index 1974519e6..809428b78 100644 --- a/lua/go/gotest.lua +++ b/lua/go/gotest.lua @@ -30,8 +30,7 @@ local short_opts = 'a:cC:b:fFmn:pst:r:v' local bench_opts = { '-benchmem', '-cpuprofile', 'profile.out' } local is_windows = utils.is_windows() -local is_git_shell = is_windows - and (vim.fn.exists('$SHELL') and vim.fn.expand('$SHELL'):find('bash.exe') ~= nil) +local is_git_shell = is_windows and (vim.fn.exists('$SHELL') and vim.fn.expand('$SHELL'):find('bash.exe') ~= nil) M.efm = function() local indent = [[%\\%( %\\)]] local efm = [[%-G=== RUN %.%#]] @@ -514,9 +513,7 @@ M.test_func = function(...) if not p then -- require('nvim-treesitter.install').commands.TSInstallSync['run!']('go') vim.notify( - 'go treesitter parser not found for file ' - .. vim.fn.bufname() - .. ' please Run `:TSInstallSync go` ', + 'go treesitter parser not found for file ' .. vim.fn.bufname() .. ' please Run `:TSInstallSync go` ', vim.log.levels.WARN ) end @@ -561,9 +558,7 @@ M.get_test_cases = function() vim.notify('sed not found', vim.log.levels.WARN) return end - local cmd = [[cat ]] - .. fpath - .. [[| sed -n 's/func\s\+\(Test.*\)(.*/\1/p' | xargs | sed 's/ /\\|/g']] + local cmd = [[cat ]] .. fpath .. [[| sed -n 's/func\s\+\(Test.*\)(.*/\1/p' | xargs | sed 's/ /\\|/g']] local tests_results = vfn.systemlist(cmd) if vim.v.shell_error ~= 0 then utils.warn('go test failed' .. cmd .. vim.inspect(tests_results)) diff --git a/lua/go/mcp/client.lua b/lua/go/mcp/client.lua index a0c225d5d..a5b7610e3 100644 --- a/lua/go/mcp/client.lua +++ b/lua/go/mcp/client.lua @@ -1,5 +1,5 @@ local M = {} -local uv = vim.uv or vim.loop +local uv = vim.uv local log = require('go.utils').log local json = vim.json diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index 91508d8cc..b36d624b6 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -2,14 +2,34 @@ local M = {} local mcp_context = require('go.mcp.context') local log = require('go.utils').log ---- Build the enriched prompt with diff + semantic context ----@param diff_text string the git diff ----@param semantic_context string gathered MCP context ----@param opts table ----@return string the full prompt for AI -local function build_enriched_prompt(diff_text, semantic_context, opts) - return string.format( - [[ +-- stylua: ignore start +local enriched_prompt_short = [[ +You are an experienced Go code reviewer. Review the code changes using the semantic context from gopls to assess impact on callers, interfaces, and downstream consumers. + +## Git Diff +```diff +%s +``` + +## Semantic Context for Changed Symbols +%s + +Focus on: bugs, correctness, error handling, concurrency issues, resource leaks, and breaking changes to callers/interfaces. + +Output as JSON array of objects with fields: +- "file": filename +- "line": line number (integer) +- "col": column number (integer, default 1) +- "severity": "error" | "warning" | "info" +- "violation": short violation label +- "principle": "[Effective Go]" | "[100 Go Mistakes]" | "[Google Style]" +- "message": description of the issue +- "refactor": brief suggestion (optional, empty string if not applicable) + +If no issues found, return: [] +]] + +local enriched_prompt_full = [[ You are an experienced Golang code reviewer with access to MCP tools. Your task is to review Go language source code for correctness, readability, performance, best practices, and style using all available context. You have access to **semantic context** gathered from gopls β€” this includes @@ -89,6 +109,8 @@ When reviewing, reason step-by-step about each aspect of the code before conclud - Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). - t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. - Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. +- Mocking: Prefer interface-based design for testability. For external dependencies, use in-memory implementations or test servers instead of complex mocking frameworks. +- Logging: Use t.Log for test logs, not global loggers. Logs should be relevant to the test case and not contain sensitive information. - Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). ## Global State & Dependencies @@ -129,10 +151,17 @@ Output as JSON array of objects with fields: - "refactor": brief refactored code or suggestion (optional, empty string if not applicable) If no issues found, return: [] -]], - diff_text, - semantic_context - ) +]] +-- stylua: ignore end + +--- Build the enriched prompt with diff + semantic context +---@param diff_text string the git diff +---@param semantic_context string gathered MCP context +---@param opts table optional; opts.brief=true selects compact prompt +---@return string the full prompt for AI +local function build_enriched_prompt(diff_text, semantic_context, opts) + local template = (opts and opts.brief) and enriched_prompt_short or enriched_prompt_full + return string.format(template, diff_text, semantic_context) end --- Enhanced code review with semantic context from the running gopls LSP @@ -163,6 +192,8 @@ function M.review(opts) vim.notify('[GoReview]: gathering semantic context from gopls...', vim.log.levels.INFO) + local brief = opts.brief or false + local function send_review(sys_prompt, semantic_ctx) local prompt = build_enriched_prompt(code_text, semantic_ctx, opts) ai.request(sys_prompt, prompt, { max_tokens = 4096, temperature = 0 }, function(response) @@ -175,7 +206,8 @@ function M.review(opts) mcp_context.gather_diff_context(code_text, function(semantic_ctx) vim.schedule(function() vim.notify('go.nvim [Review]: semantic context ready. Sending to AI...', vim.log.levels.INFO) - send_review(ai.diff_review_system_prompt, semantic_ctx) + local sys = brief and ai.diff_review_system_prompt_short or ai.diff_review_system_prompt + send_review(sys, semantic_ctx) end) end) else @@ -184,7 +216,8 @@ function M.review(opts) mcp_context.gather_buffer_context(bufnr, function(semantic_ctx) vim.schedule(function() vim.notify('go.nvim [Review]: semantic context ready. Sending to AI...', vim.log.levels.INFO) - send_review(ai.code_review_system_prompt, semantic_ctx) + local sys = brief and ai.code_review_system_prompt_short or ai.code_review_system_prompt + send_review(sys, semantic_ctx) end) end) end From f2d2c09965034b745cf2de98469753be50553de3 Mon Sep 17 00:00:00 2001 From: ray-x Date: Tue, 10 Mar 2026 13:56:17 +1100 Subject: [PATCH 09/21] error handlings --- lua/go/ai.lua | 5 +++++ lua/go/mcp/review.lua | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/lua/go/ai.lua b/lua/go/ai.lua index ee80b2592..1d01e8e7a 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -628,11 +628,15 @@ When reviewing, reason step-by-step about each aspect of the code before conclud ## Concurrency & Errors - Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel. - Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). - Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. - Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. - Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. - Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. +- Handle error cases first (left-aligned), then the successful path. Avoid deep nesting of if statements for the happy path. Reduce `if err != nil` nesting by returning early. +- Errors should only be handled once β€” avoid patterns where errors are checked, annotated, and returned in multiple layers. +- Use traceID or context values for cross-cutting concerns instead of passing through multiple layers of error annotations. ## Documentation & API Design (Google Style) - Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. @@ -792,6 +796,7 @@ IMPORTANT: Use the NEW file line numbers from the diff hunk headers (the second ## Concurrency & Errors - Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel. - Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). - Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. - Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index b36d624b6..b5dba34fc 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -91,11 +91,15 @@ When reviewing, reason step-by-step about each aspect of the code before conclud ## Concurrency & Errors - Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel, which can cause panics. - Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). - Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. - Error Wrapping: Ensure %%w is used (not %%v) when callers need to inspect wrapped errors. Place %%w at the end of the format string. Avoid redundant annotations (e.g., "failed: %%v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. - Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. - Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. +- Handle error cases first (left-aligned), then the successful path. Avoid deep nesting of if statements for the happy path. Reduce `if err != nil` nesting by returning early. +- Errors should only be handled once β€” avoid patterns where errors are checked, annotated, and returned in multiple layers. +- Use traceID or context values for cross-cutting concerns instead of passing through multiple layers of error annotations. ## Documentation & API Design (Google Style) - Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. From 409764d49697617d1c22f5d6e2fc186cf7b52bc8 Mon Sep 17 00:00:00 2001 From: ray-x Date: Tue, 10 Mar 2026 15:31:51 +1100 Subject: [PATCH 10/21] add prompts.lua --- lua/go/ai.lua | 171 ++--------------------------------- lua/go/mcp/context.lua | 59 ++++++++++-- lua/go/mcp/review.lua | 198 +++++++++++------------------------------ lua/go/prompts.lua | 100 +++++++++++++++++++++ 4 files changed, 210 insertions(+), 318 deletions(-) create mode 100644 lua/go/prompts.lua diff --git a/lua/go/ai.lua b/lua/go/ai.lua index 1d01e8e7a..cadd72869 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -6,6 +6,7 @@ local M = {} local utils = require('go.utils') local log = utils.log +local prompts = require('go.prompts') -- Cached Copilot API token local _copilot_token = nil @@ -582,93 +583,14 @@ local function get_git_diff(filepath, branch, callback) end) end +-- stylua: ignore start local code_review_system_prompt = [[You are an experienced Golang code reviewer. Your task is to review Go language source code for correctness, readability, performance, best practices, and style. Carefully analyze the given Go code snippet or file and provide specific actionable feedback to improve quality. Identify issues such as bugs, inefficient constructs, poor naming, inconsistent formatting, concurrency pitfalls, error handling mistakes, or deviations from idiomatic Go. Suggest precise code changes and explain why they improve the code. When reviewing, reason step-by-step about each aspect of the code before concluding. Be polite, professional, and constructive. - -# Audit Categories - -1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. -2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. -3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. -4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. -5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. -6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %w. -7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. -8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. -9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. -10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. - -# Go-Specific Review Dimensions - -## Formatting & Naming (Effective Go / Google Style) -- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). -- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. -- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). -- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. -- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. -- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. - -## Initialization & Control (The "Go Way") -- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. -- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). -- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). -- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). -- Prefer := for non-zero initialization, var for zero-value declarations. -- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. - -## Data Integrity & Memory (100 Go Mistakes) -- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. -- Conversions: Ensure string-to-slice conversions are necessary and efficient. -- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. -- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. -- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. -- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). - -## Concurrency & Errors -- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. -- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel. -- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). -- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. -- Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. -- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. -- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. -- Handle error cases first (left-aligned), then the successful path. Avoid deep nesting of if statements for the happy path. Reduce `if err != nil` nesting by returning early. -- Errors should only be handled once β€” avoid patterns where errors are checked, annotated, and returned in multiple layers. -- Use traceID or context values for cross-cutting concerns instead of passing through multiple layers of error annotations. - -## Documentation & API Design (Google Style) -- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. -- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). -- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. -- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. -- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. - -## Testing (Google Style) -- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. -- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). -- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. -- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. -- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). -- Mocking: For external dependencies, prefer interface-based design that allows manual mocks. Flag overuse of complex mocking frameworks that generate code or require heavy setup. -- Logging: Avoid log output in tests except for debugging. Use t.Log for test logs, which are only shown on failure or with verbose flag. -- Avoid shared state between tests. Each test should be independent and repeatable. - -## Global State & Dependencies -- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. -- Flag service locator patterns and thick-client singletons. - -## String Handling (Google Style) -- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. -- Use backticks for constant multi-line strings. - -# Output Instructions - -For every critique, provide: -1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). -2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. -3. A brief refactored code suggestion where applicable. +]] + .. prompts.review_guidelines() + .. [[ # Instructions @@ -754,85 +676,9 @@ local diff_review_system_prompt = [[You are an experienced Golang code reviewer. You are reviewing a unified diff (git diff) of Go source code changes against a base branch. Focus ONLY on the changed lines (lines starting with + or context around them). Evaluate the changes for correctness, readability, performance, best practices, and style. IMPORTANT: Use the NEW file line numbers from the diff hunk headers (the second number in @@ -a,b +c,d @@). For added/changed lines (starting with +), compute the actual file line number by counting from the hunk start. - -# Audit Categories - -1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. -2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. -3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. -4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. -5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. -6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %w. -7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. -8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. -9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. -10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. - -# Go-Specific Review Dimensions - -## Formatting & Naming (Effective Go / Google Style) -- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). -- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. -- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). -- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. -- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. -- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. - -## Initialization & Control (The "Go Way") -- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. -- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). -- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). -- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). -- Prefer := for non-zero initialization, var for zero-value declarations. -- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. - -## Data Integrity & Memory (100 Go Mistakes) -- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. -- Conversions: Ensure string-to-slice conversions are necessary and efficient. -- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. -- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. -- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. -- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). - -## Concurrency & Errors -- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. -- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel. -- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). -- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. -- Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. -- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. -- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. - -## Documentation & API Design (Google Style) -- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. -- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). -- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. -- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. -- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. - -## Testing (Google Style) -- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. -- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). -- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. -- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. -- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). -- Mocking: Prefer interfaces for dependencies to allow mocking. Flag untestable code with hard-coded dependencies or side effects. -- Logging: Avoid log package in tests; use t.Log for test output to integrate with test reporting. - -## Global State & Dependencies -- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. -- Flag service locator patterns and thick-client singletons. - -## String Handling (Google Style) -- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. -- Use backticks for constant multi-line strings. - -# Output Instructions - -For every critique, provide: -1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). -2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. -3. A brief refactored code suggestion where applicable. +]] + .. prompts.review_guidelines() + .. [[ # Instructions @@ -862,6 +708,7 @@ Rules: - Line numbers must be the NEW file line numbers (post-change), 1-based. - Focus on practical, specific improvements only. ]] +-- stylua: ignore end --- Parse quickfix lines from the LLM response and open the quickfix list. --- Lines not matching the format are silently skipped. diff --git a/lua/go/mcp/context.lua b/lua/go/mcp/context.lua index 004528d5f..61ba34728 100644 --- a/lua/go/mcp/context.lua +++ b/lua/go/mcp/context.lua @@ -197,19 +197,40 @@ function M.get_symbol_context_via_lsp(bufnr, line, col, callback) ref_params.context = { includeDeclaration = false } vim.lsp.buf_request(bufnr, 'textDocument/references', ref_params, function(err, result) if not err and result and #result > 0 then - local refs = {} + -- Partition into non-test (high value) and test refs, dedup both + local non_test_refs = {} + local test_refs = {} local seen = {} for _, ref in ipairs(result) do - local fname = vim.fn.fnamemodify(vim.uri_to_fname(ref.uri), ':.') + local fpath = vim.uri_to_fname(ref.uri) + local fname = vim.fn.fnamemodify(fpath, ':.') local key = string.format('- %s:%d', fname, ref.range.start.line + 1) if not seen[key] then seen[key] = true - table.insert(refs, format_ref_location(ref.uri, ref.range.start.line)) + if is_test_file(fname) then + table.insert(test_refs, ref) + else + table.insert(non_test_refs, ref) + end + end + end + -- Show non-test refs first, then fill remaining slots with test refs + local max_refs = 15 + local refs = {} + for _, ref in ipairs(non_test_refs) do + table.insert(refs, format_ref_location(ref.uri, ref.range.start.line)) + if #refs >= max_refs then + break end - if #refs >= 10 then - table.insert(refs, string.format(' ... and %d more', #result - 10)) + end + local remaining = max_refs - #refs + for idx, ref in ipairs(test_refs) do + if idx > remaining then + local skipped = #test_refs - remaining + table.insert(refs, string.format(' ... and %d more test refs', skipped)) break end + table.insert(refs, format_ref_location(ref.uri, ref.range.start.line)) end table.insert(results, '\n* References (' .. #result .. '):\n' .. table.concat(refs, '\n')) end @@ -224,16 +245,38 @@ function M.get_symbol_context_via_lsp(bufnr, line, col, callback) end vim.lsp.buf_request(bufnr, 'callHierarchy/incomingCalls', { item = result[1] }, function(err2, calls) if not err2 and calls and #calls > 0 then - local callers = {} + -- Partition callers: non-test first + local non_test_calls = {} + local test_calls = {} for _, call in ipairs(calls) do + local fname = vim.fn.fnamemodify(vim.uri_to_fname(call.from.uri), ':.') + if is_test_file(fname) then + table.insert(test_calls, call) + else + table.insert(non_test_calls, call) + end + end + local max_callers = 15 + local callers = {} + for _, call in ipairs(non_test_calls) do table.insert(callers, format_caller_location( call.from.uri, call.from.range.start.line, call.from.name )) - if #callers >= 15 then - table.insert(callers, string.format(' ... and %d more', #calls - 15)) + if #callers >= max_callers then break end end + local remaining = max_callers - #callers + for idx, call in ipairs(test_calls) do + if idx > remaining then + local skipped = #test_calls - remaining + table.insert(callers, string.format(' ... and %d more test callers', skipped)) + break + end + table.insert(callers, format_caller_location( + call.from.uri, call.from.range.start.line, call.from.name + )) + end table.insert(results, '\n* Callers (' .. #calls .. '):\n' .. table.concat(callers, '\n')) end check_done() diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index b5dba34fc..75f65315d 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -1,171 +1,75 @@ local M = {} local mcp_context = require('go.mcp.context') local log = require('go.utils').log +local prompts = require('go.prompts') --- stylua: ignore start -local enriched_prompt_short = [[ -You are an experienced Go code reviewer. Review the code changes using the semantic context from gopls to assess impact on callers, interfaces, and downstream consumers. - -## Git Diff -```diff -%s -``` - -## Semantic Context for Changed Symbols -%s - -Focus on: bugs, correctness, error handling, concurrency issues, resource leaks, and breaking changes to callers/interfaces. +-- JSON output format shared by all MCP review prompts +local json_output_format = [[ Output as JSON array of objects with fields: - "file": filename - "line": line number (integer) - "col": column number (integer, default 1) - "severity": "error" | "warning" | "info" -- "violation": short violation label -- "principle": "[Effective Go]" | "[100 Go Mistakes]" | "[Google Style]" +- "violation": short violation label (e.g., "Non-idiomatic naming", "Slice memory leak") +- "principle": one of "[Effective Go]", "[100 Go Mistakes]", "[Google Style]" - "message": description of the issue -- "refactor": brief suggestion (optional, empty string if not applicable) +- "refactor": brief refactored code or suggestion (optional, empty string if not applicable) If no issues found, return: [] ]] -local enriched_prompt_full = [[ -You are an experienced Golang code reviewer with access to MCP tools. Your task is to review Go language source code for correctness, readability, performance, best practices, and style using all available context. - -You have access to **semantic context** gathered from gopls β€” this includes -information about callers, references, and interface implementations for -every changed symbol. Use this to assess the **full impact** of the changes: -- Are callers affected by signature changes? -- Do interface contracts still hold? -- Are there downstream consumers that might break? -- Are error handling patterns consistent with existing callers? - -## Git Diff -```diff -%s -``` - -## Semantic Context for Changed Symbols -%s +-- stylua: ignore start +local mcp_code_review_system = + [[You are an experienced Go code reviewer with access to semantic context from gopls. +Your task is to review Go source code for correctness, readability, performance, best practices, and style. +Use the semantic context to assess impact on callers, interfaces, and downstream consumers. When reviewing, reason step-by-step about each aspect of the code before concluding. Be polite, professional, and constructive. +]] + .. prompts.review_guidelines() .. json_output_format -# Audit Categories - -1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. -2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. -3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. -4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. -5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. -6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %%w. -7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. -8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. -9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. -10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis. - -# Go-Specific Review Dimensions - -## Formatting & Naming (Effective Go / Google Style) -- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). -- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. -- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). -- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. -- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. -- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. - -## Initialization & Control (The "Go Way") -- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. -- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). -- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). -- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). -- Prefer := for non-zero initialization, var for zero-value declarations. -- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. - -## Data Integrity & Memory (100 Go Mistakes) -- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. -- Conversions: Ensure string-to-slice conversions are necessary and efficient. -- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. -- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. -- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. -- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). - -## Concurrency & Errors -- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. -- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel, which can cause panics. -- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). -- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. -- Error Wrapping: Ensure %%w is used (not %%v) when callers need to inspect wrapped errors. Place %%w at the end of the format string. Avoid redundant annotations (e.g., "failed: %%v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. -- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. -- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. -- Handle error cases first (left-aligned), then the successful path. Avoid deep nesting of if statements for the happy path. Reduce `if err != nil` nesting by returning early. -- Errors should only be handled once β€” avoid patterns where errors are checked, annotated, and returned in multiple layers. -- Use traceID or context values for cross-cutting concerns instead of passing through multiple layers of error annotations. - -## Documentation & API Design (Google Style) -- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. -- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). -- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. -- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. -- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. - -## Testing (Google Style) -- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. -- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). -- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. -- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. -- Mocking: Prefer interface-based design for testability. For external dependencies, use in-memory implementations or test servers instead of complex mocking frameworks. -- Logging: Use t.Log for test logs, not global loggers. Logs should be relevant to the test case and not contain sensitive information. -- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). - -## Global State & Dependencies -- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. -- Flag service locator patterns and thick-client singletons. - -## String Handling (Google Style) -- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. -- Use backticks for constant multi-line strings. - -# Output Instructions - -For every critique, provide: -1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). -2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. -3. A brief refactored code suggestion where applicable. - -# Instructions +local mcp_code_review_system_short = + [[You are an experienced Go code reviewer with semantic context from gopls. +Focus on: bugs, correctness, error handling, concurrency issues, resource leaks, and breaking changes to callers/interfaces. +]] .. json_output_format -1. Read the entire Go code provided. -2. Use MCP tools to gather additional context if available (e.g., read related files, check project structure). -3. For each audit category above, check whether any issues apply. -4. For each Go-specific review dimension, check whether any issues apply. -5. Assess functionality and correctness. -6. Evaluate code readability and style against Go conventions. -7. Check for performance or concurrency issues. -8. Review error handling and package usage. -9. Provide only actionable improvements β€” skip praise or explanations of what is already good. +local mcp_diff_review_system = + [[You are an experienced Go code reviewer with access to semantic context from gopls. +You are reviewing a unified diff of Go source code changes. Focus on the changed lines. +Use the semantic context to assess impact on callers, interfaces, and downstream consumers. -Output as JSON array of objects with fields: -- "file": filename -- "line": line number (integer) -- "col": column number (integer, default 1) -- "severity": "error" | "warning" | "info" -- "violation": short violation label (e.g., "Non-idiomatic naming", "Slice memory leak") -- "principle": one of "[Effective Go]", "[100 Go Mistakes]", "[Google Style]" -- "message": description of the issue -- "refactor": brief refactored code or suggestion (optional, empty string if not applicable) - -If no issues found, return: [] +When reviewing, reason step-by-step about each aspect of the code before concluding. Be polite, professional, and constructive. ]] + .. prompts.review_guidelines() .. json_output_format + +local mcp_diff_review_system_short = + [[You are an experienced Go code reviewer with semantic context from gopls. +Review the unified diff for bugs, correctness, error handling, concurrency, and breaking changes in the changed lines only. +]] .. json_output_format -- stylua: ignore end ---- Build the enriched prompt with diff + semantic context ----@param diff_text string the git diff +--- Build the enriched user prompt with diff/code + semantic context +---@param code_text string the code or diff text ---@param semantic_context string gathered MCP context ----@param opts table optional; opts.brief=true selects compact prompt ----@return string the full prompt for AI -local function build_enriched_prompt(diff_text, semantic_context, opts) - local template = (opts and opts.brief) and enriched_prompt_short or enriched_prompt_full - return string.format(template, diff_text, semantic_context) +---@param opts table optional; opts.diff=true for diff mode +---@return string the user prompt for AI +local function build_enriched_prompt(code_text, semantic_context, opts) + local parts = {} + if opts and opts.diff then + table.insert(parts, '## Git Diff\n```diff\n' .. code_text .. '\n```') + else + table.insert(parts, '## Source Code\n```go\n' .. code_text .. '\n```') + end + table.insert(parts, '\n## Semantic Context for Changed Symbols\n' .. semantic_context) + if not opts or not opts.brief then + table.insert(parts, '\nUse the semantic context to assess the full impact:') + table.insert(parts, '- Are callers affected by signature changes?') + table.insert(parts, '- Do interface contracts still hold?') + table.insert(parts, '- Are there downstream consumers that might break?') + table.insert(parts, '- Are error handling patterns consistent with existing callers?') + end + return table.concat(parts, '\n') end --- Enhanced code review with semantic context from the running gopls LSP @@ -206,21 +110,19 @@ function M.review(opts) end if opts.diff then - -- Use the NEW function from context.lua mcp_context.gather_diff_context(code_text, function(semantic_ctx) vim.schedule(function() vim.notify('go.nvim [Review]: semantic context ready. Sending to AI...', vim.log.levels.INFO) - local sys = brief and ai.diff_review_system_prompt_short or ai.diff_review_system_prompt + local sys = brief and mcp_diff_review_system_short or mcp_diff_review_system send_review(sys, semantic_ctx) end) end) else local bufnr = vim.api.nvim_get_current_buf() - -- Use the NEW function from context.lua mcp_context.gather_buffer_context(bufnr, function(semantic_ctx) vim.schedule(function() vim.notify('go.nvim [Review]: semantic context ready. Sending to AI...', vim.log.levels.INFO) - local sys = brief and ai.code_review_system_prompt_short or ai.code_review_system_prompt + local sys = brief and mcp_code_review_system_short or mcp_code_review_system send_review(sys, semantic_ctx) end) end) diff --git a/lua/go/prompts.lua b/lua/go/prompts.lua new file mode 100644 index 000000000..c14fe3e9f --- /dev/null +++ b/lua/go/prompts.lua @@ -0,0 +1,100 @@ +-- Shared prompt building blocks for Go code review (used by ai.lua and mcp/review.lua) + +local M = {} + +-- stylua: ignore start + +M.audit_categories = [[ +# Audit Categories + +1. Code Organization: Misaligned project structure, init function abuse, or getter/setter overkill. +2. Data Types: Octal literals, integer overflows, floating-point inaccuracies, and slice/map header confusion. +3. Control Structures: Range loop pointer copies, using break in switch inside for loops, and map iteration non-determinism. +4. String Handling: Inefficient concatenation, len() vs. rune count, and substring memory leaks. +5. Functions & Methods: Pointer vs. value receivers, named result parameters, and returning nil interfaces. +6. Error Management: Panic/recover abuse, ignoring errors, and failing to wrap errors with %w. +7. Concurrency: Goroutine leaks, context misuse, data races, and sync vs. channel trade-offs. +8. Standard Library: http body closing, json marshaling pitfalls, and time.After leaks. +9. Testing: Table-driven test errors, race conditions in tests, and external dependency mocking. +10. Optimizations: CPU cache misalignment, false sharing, and stack vs. heap escape analysis.]] + +M.review_dimensions = [[ +# Go-Specific Review Dimensions + +## Formatting & Naming (Effective Go / Google Style) +- Indentation/Formatting: Check for non-standard layouts (assume gofmt standards). +- Naming: Enforce short, pithy names for local variables (e.g., r for reader) and MixedCaps/Exported naming conventions. +- Interface Names: Ensure one-method interfaces end in an "er" suffix (e.g., Reader, Writer). +- Function/Method Naming: Avoid repeating package name (e.g., yamlconfig.Parse not yamlconfig.ParseYAMLConfig), receiver type, parameter names, or return types in the function name. +- No Get Prefix: Functions returning values should use noun-like names without "Get" prefix (e.g., JobName not GetJobName). Functions doing work should use verb-like names. +- Util Packages: Flag packages named "util", "helper", "common" β€” names should describe what the package provides. + +## Initialization & Control (The "Go Way") +- Redeclaring vs. Reassigning: Identify where := is used correctly vs. where it creates shadowing bugs. Flag shadowing of variables in inner scopes (especially context, error) that silently creates new variables instead of updating the outer one. +- Do not shadow standard package names (e.g., using "url" as a variable name blocks net/url). +- The Switch Power: Look for complex if-else chains that should be simplified into Go's powerful switch (which handles multiple expressions and comparisons). +- Allocation: Differentiate between new (zeroed memory pointer) and make (initialized slice/map/chan). +- Prefer := for non-zero initialization, var for zero-value declarations. +- Signal Boosting: Flag easy-to-miss "err == nil" checks (positive error checks) β€” these should have a clarifying comment. + +## Data Integrity & Memory (100 Go Mistakes) +- Slice/Map Safety: Check for sub-slice memory leaks and map capacity issues. +- Conversions: Ensure string-to-slice conversions are necessary and efficient. +- Backing Arrays: Flag cases where multiple slices share a backing array unintentionally. +- Size Hints: For performance-sensitive code, check if make() should have capacity hints for slices/maps when the size is known. +- Channel Direction: Ensure channel parameters specify direction (<-chan or chan<-) where possible. +- Map Initialization: Flag writes to nil maps (maps must be initialized with make before mutation, though reads are safe). + +## Concurrency & Errors +- Communication: "Do not communicate by sharing memory; instead, share memory by communicating." Flag excessive Mutex use where Channels would be cleaner. +- Only sender can close a channel: Flag cases where multiple goroutines might close the same channel, which can cause panics. +- Error Handling: Check for the "Happy Path" (return early on errors to keep the successful logic left-aligned). +- Error Structure: Flag string-matching on error messages β€” use sentinel errors, errors.Is, or errors.As instead. +- Error Wrapping: Ensure %w is used (not %v) when callers need to inspect wrapped errors. Place %w at the end of the format string. Avoid redundant annotations (e.g., "failed: %v" adds nothing β€” just return err). Do not duplicate information the underlying error already provides. +- Panic/Recover: Ensure panic is only used for truly unrecoverable setup errors or API misuse, not for flow control. Panics must never escape package boundaries in libraries β€” use deferred recover at public API boundaries. +- Do not call log.Fatal or t.Fatal from goroutines other than the main test goroutine. +- Handle error cases first (left-aligned), then the successful path. Avoid deep nesting of if statements for the happy path. Reduce `if err != nil` nesting by returning early. +- Errors should only be handled once β€” avoid patterns where errors are checked, annotated, and returned in multiple layers. +- Use traceID or context values for cross-cutting concerns instead of passing through multiple layers of error annotations. + +## Documentation & API Design (Google Style) +- Context conventions: Do not restate that cancelling ctx stops the function (it is implied). Document only non-obvious context behavior. +- Cleanup: Exported constructors/functions that acquire resources must document how to release them (e.g., "Call Stop to release resources when done"). +- Concurrency safety: Document non-obvious concurrency properties. Read-only operations are assumed safe; mutating operations are assumed unsafe. Document exceptions. +- Error documentation: Document significant sentinel errors and error types returned by functions, including whether they are pointer receivers. +- Function argument lists: Flag functions with too many parameters. Recommend option structs or variadic options pattern for complex configuration. + +## Testing (Google Style) +- Leave testing to the Test function: Flag assertion helper libraries β€” prefer returning errors or using cmp.Diff with clear failure messages in the Test function itself. +- Table-driven tests: Use field names in struct literals. Keep setup scoped to tests that need it (no global init for test data). +- t.Fatal usage: Use t.Fatal only for setup failures. In table-driven subtests, use t.Fatal inside t.Run; outside subtests, use t.Error + continue. +- Do not call t.Fatal from separate goroutines β€” use t.Error and return instead. +- Test doubles: Follow naming conventions (package suffixed with "test", types named by behavior like AlwaysCharges). +- Mocking: Prefer interface-based design for testability. For external dependencies, use in-memory implementations or test servers instead of complex mocking frameworks. +- Logging: Use t.Log for test logs, not global loggers. Test logs are only shown on failure or with verbose flag. +- Avoid shared state between tests. Each test should be independent and repeatable. + +## Global State & Dependencies +- Flag package-level mutable state (global vars, registries, singletons). Prefer instance-based APIs with explicit dependency passing. +- Flag service locator patterns and thick-client singletons. + +## String Handling (Google Style) +- Prefer "+" for simple concatenation, fmt.Sprintf for formatting, strings.Builder for piecemeal construction. +- Use backticks for constant multi-line strings.]] + +M.output_critique_format = [[ +# Output Instructions + +For every critique, provide: +1. The Violation (e.g., "Non-idiomatic naming" or "Slice memory leak"). +2. The Principle: Cite if it is an [Effective Go] rule, a [100 Go Mistakes] pitfall, or a [Google Style] convention. +3. A brief refactored code suggestion where applicable.]] + +-- stylua: ignore end + +--- Build the full review guidelines block (audit + dimensions + output instructions) +function M.review_guidelines() + return M.audit_categories .. '\n' .. M.review_dimensions .. '\n' .. M.output_critique_format +end + +return M From ea37cd8c164995e4ad67b2bcd2319b82478e8caa Mon Sep 17 00:00:00 2001 From: ray-x Date: Tue, 10 Mar 2026 17:58:39 +1100 Subject: [PATCH 11/21] fix go.ai.code_review --- lua/go/commands.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/go/commands.lua b/lua/go/commands.lua index 64e98c5a2..82bcc4530 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -717,15 +717,14 @@ return { end create_cmd('GoCodeReview', function(args) - local opts = parse_review_args(args) - -- Use MCP-enhanced review when available local go_cfg = require('go').config() or {} if go_cfg.mcp and go_cfg.mcp.enable then + local opts = parse_review_args(args) require('go.mcp.review').review(opts) else - -- Fallback to existing review without MCP - require('go.ai.review').review(opts) + -- Fallback: ai.code_review does its own arg parsing from raw command opts + require('go.ai').code_review(args) end end, { nargs = '*', From ceceef8ae93c40fa42450680abeef8784e304de9 Mon Sep 17 00:00:00 2001 From: ray-x Date: Sat, 14 Mar 2026 21:21:38 +1100 Subject: [PATCH 12/21] capture function scope --- lua/go/ai.lua | 137 ++++++++++++++++++++++++++++++++++++------ lua/go/commands.lua | 116 ++++++++++++++++++++++++++++++++--- lua/go/mcp/review.lua | 3 + 3 files changed, 229 insertions(+), 27 deletions(-) diff --git a/lua/go/ai.lua b/lua/go/ai.lua index cadd72869..f352be718 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -179,7 +179,7 @@ GOPLS LSP COMMANDS (via GoGopls [json_args]): AI-POWERED: - GoCmtAI β€” Generate doc comment for the declaration at cursor using AI -- GoCodeReview β€” Review the current Go file (or visual selection) with AI; outputs findings to the vim quickfix list +- GoCodeReview β€” Review the current Go file (or visual selection) with AI; outputs findings to the vim quickfix list. Args: -d [branch] (diff mode), -b (brief), -m (change description) - GoDocAI [query] β€” Find a function/type by vague name and generate rich AI documentation from its source code ]] @@ -784,12 +784,13 @@ local function handle_review_response(response, filename) vim.notify(string.format('[GoCodeReview]: %d issue(s) added to quickfix', #qflist), vim.log.levels.INFO) end ---- Entry point for :GoCodeReview [-d [branch]] +--- Entry point for :GoCodeReview [-d [branch]] [-b] [-m ] --- Reviews the current buffer, visual selection, or diff against a branch. --- :GoCodeReview β€” review entire file --- :'<,'>GoCodeReview β€” review visual selection --- :GoCodeReview -d β€” review only changes vs main/master (auto-detected) --- :GoCodeReview -d develop β€” review only changes vs 'develop' +--- :GoCodeReview -m add lru cache and remove fifo cache β€” provide change context --- @param opts table Standard nvim command opts (range, line1, line2, fargs) function M.code_review(opts) local cfg = _GO_NVIM_CFG.ai or {} @@ -803,10 +804,11 @@ function M.code_review(opts) local fargs = (type(opts) == 'table' and opts.fargs) or {} - -- Parse flags: -d [branch], -b/--brief + -- Parse flags: -d [branch], -b/--brief, -m local diff_mode = false local diff_branch = nil local brief = false + local change_message = nil local i = 1 while i <= #fargs do local arg = fargs[i] @@ -818,6 +820,16 @@ function M.code_review(opts) end elseif arg == '-b' or arg == '--brief' then brief = true + elseif arg == '-m' or arg == '--message' then + -- Collect everything after -m as the change description + local msg_parts = {} + for j = i + 1, #fargs do + table.insert(msg_parts, fargs[j]) + end + local raw = table.concat(msg_parts, ' ') + -- Convert literal \n sequences to real newlines + change_message = raw:gsub('\\n', '\n') + break end i = i + 1 end @@ -833,7 +845,11 @@ function M.code_review(opts) return end local short_name = vim.fn.expand('%:t') - local user_msg = string.format('File: %s\nBase branch: %s\n\n```diff\n%s\n```', short_name, branch, diff) + local user_msg = '' + if change_message and change_message ~= '' then + user_msg = '## Change Description\n' .. change_message .. '\n\n' + end + user_msg = user_msg .. string.format('File: %s\nBase branch: %s\n\n```diff\n%s\n```', short_name, branch, diff) local sys = brief and diff_review_system_prompt_short or diff_review_system_prompt M.request(sys, user_msg, { max_tokens = 1500, temperature = 0 }, function(resp) handle_review_response(resp, filename) @@ -864,7 +880,11 @@ function M.code_review(opts) end local code = table.concat(numbered, '\n') local short_name = vim.fn.expand('%:t') - local user_msg = string.format('File: %s\n\n```go\n%s\n```', short_name, code) + local user_msg = '' + if change_message and change_message ~= '' then + user_msg = '## Change Description\n' .. change_message .. '\n\n' + end + user_msg = user_msg .. string.format('File: %s\n\n```go\n%s\n```', short_name, code) vim.notify('[GoCodeReview]: reviewing …', vim.log.levels.INFO) @@ -950,6 +970,44 @@ local function build_chat_user_msg(question, code, lang) return question end +--- Get the enclosing function/method node and its text from the buffer. +--- Returns (func_text, func_name) or (nil, nil) if cursor is not inside a function. +local function get_enclosing_func(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + local ok, ts_utils = pcall(require, 'nvim-treesitter.ts_utils') + if not ok then + ok, ts_utils = pcall(require, 'guihua.ts_obsolete.ts_utils') + if not ok then + return nil, nil + end + end + local current_node = ts_utils.get_node_at_cursor() + if not current_node then + return nil, nil + end + local expr = current_node + while expr do + if expr:type() == 'function_declaration' or expr:type() == 'method_declaration' then + break + end + expr = expr:parent() + end + if not expr then + return nil, nil + end + + local func_text = vim.treesitter.get_node_text(expr, bufnr) or '' + + -- Extract function name + local name_node = expr:field('name') + local func_name = '' + if name_node and name_node[1] then + func_name = vim.treesitter.get_node_text(name_node[1], bufnr) or '' + end + + return func_text, func_name +end + --- Entry point for :GoAIChat --- @param opts table Standard nvim command opts function M.chat(opts) @@ -965,27 +1023,30 @@ function M.chat(opts) local fargs = (type(opts) == 'table' and opts.fargs) or {} local question = vim.trim(table.concat(fargs, ' ')) - -- Collect visual selection or surrounding function context local code = nil local lang = vim.bo.filetype or 'go' + local bufnr = vim.api.nvim_get_current_buf() + local func_name = nil + -- 1. Visual selection: send selected code if type(opts) == 'table' and opts.range and opts.range == 2 then - -- Visual selection local sel_lines = vim.api.nvim_buf_get_lines(0, opts.line1 - 1, opts.line2, false) code = table.concat(sel_lines, '\n') - end - local diff_text - -- create git comments require git diff context - if string.find(question, 'create a commit summary') then - -- get diff against master/main - local branch = 'master' - if string.find(question, 'main') then - branch = 'main' + -- 2. Cursor inside a function: send the function text + elseif lang == 'go' then + local func_text, fname = get_enclosing_func(bufnr) + if func_text and func_text ~= '' then + code = func_text + func_name = fname end + end + -- Handle "create a commit summary" specially + local diff_text + if question:find('create a commit summary') then + local branch = question:find('main') and 'main' or 'master' local code_text = vim.fn.system({ 'git', 'diff', '-U10', branch, '--', '*.go' }) - if not code_text or #code_text == 0 then vim.notify('No code to commit', vim.log.levels.WARN) return @@ -993,6 +1054,7 @@ function M.chat(opts) diff_text = code_text end + --- Dispatch the question with code context and optional LSP references local function dispatch(q) if q == '' then vim.notify('[GoAIChat]: empty question', vim.log.levels.WARN) @@ -1005,19 +1067,56 @@ function M.chat(opts) end) end + --- Dispatch with LSP reference context appended to code + local function dispatch_with_refs(q) + if not func_name or func_name == '' then + dispatch(q) + return + end + + -- Try to gather references from LSP for richer context + local row, col = unpack(vim.api.nvim_win_get_cursor(0)) + local mcp_ok, mcp_ctx = pcall(require, 'go.mcp.context') + if not mcp_ok or not mcp_ctx.get_symbol_context_via_lsp then + dispatch(q) + return + end + + vim.notify('[GoAIChat]: gathering references …', vim.log.levels.INFO) + mcp_ctx.get_symbol_context_via_lsp(bufnr, row - 1, col, function(ref_text) + vim.schedule(function() + if ref_text and ref_text ~= '' and not ref_text:match('^%(no ') then + code = code .. '\n\n--- References / Callers ---\n' .. ref_text + end + dispatch(q) + end) + end) + end + if diff_text then return dispatch(diff_text) end + if question ~= '' then - dispatch(question) + -- If we have function context, enrich with references + if func_name and func_name ~= '' and not code:find('create a commit') then + dispatch_with_refs(question) + else + dispatch(question) + end else -- Interactive prompt + local default_q = code and 'explain this code' or '' vim.ui.input({ prompt = 'GoAIChat> ', - default = code and 'explain this code' or '', + default = default_q, }, function(input) if input and input ~= '' then - dispatch(input) + if func_name and func_name ~= '' then + dispatch_with_refs(input) + else + dispatch(input) + end end end) end diff --git a/lua/go/commands.lua b/lua/go/commands.lua index 82bcc4530..9cab58d89 100644 --- a/lua/go/commands.lua +++ b/lua/go/commands.lua @@ -693,19 +693,32 @@ return { opts.end_line = end_line end - for i, arg in ipairs(fargs) do + local i = 1 + while i <= #fargs do + local arg = fargs[i] if arg == '-d' or arg == '--diff' then opts.diff = true -- Next arg might be branch name if fargs[i + 1] and not fargs[i + 1]:match('^%-') then opts.branch = fargs[i + 1] + i = i + 1 end elseif arg == '-b' or arg == '--brief' then opts.brief = true elseif arg == '-f' or arg == '--full' then opts.full = true + elseif arg == '-m' or arg == '--message' then + -- Collect everything after -m as the change description + local msg_parts = {} + for j = i + 1, #fargs do + table.insert(msg_parts, fargs[j]) + end + local raw = table.concat(msg_parts, ' ') + -- Convert literal \n sequences to real newlines + opts.message = raw:gsub('\\n', '\n') + break end - + i = i + 1 end -- Default branch if diff mode but no branch specified @@ -716,21 +729,108 @@ return { return opts end + --- Open a floating buffer for multi-line message input using guihua textview. + --- Falls back to a plain floating window if guihua is not available. + --- Calls `on_submit(text)` with the buffer contents when the user submits. + local function open_message_input(on_submit) + local TextView = utils.load_plugin('guihua.lua', 'guihua.textview') + if TextView then + local width = math.min(80, vim.o.columns - 4) + local height = math.min(10, math.floor(vim.o.lines * 0.3)) + local win = TextView:new({ + loc = 'top_center', + rect = { height = height, width = width, pos_x = 0, pos_y = 4 }, + allow_edit = true, + enter = true, + ft = 'markdown', + title = ' Change description ( submit, q cancel) ', + title_pos = 'center', + data = { '' }, + }) + if not win or not win.buf then + vim.notify('[GoCodeReview]: failed to create input window', vim.log.levels.ERROR) + return + end + + -- Enable completion sources (Copilot, nvim-cmp) in the edit buffer + vim.api.nvim_set_option_value('buftype', '', { buf = win.buf }) + vim.api.nvim_set_option_value('filetype', 'markdown', { buf = win.buf }) + -- Force-attach copilot if available (bypasses filetype/buftype rejection) + local copilot_ok, copilot_cmd = pcall(require, 'copilot.command') + if copilot_ok and copilot_cmd.attach then + copilot_cmd.attach({ force = true, bufnr = win.buf }) + end + + local submitted = false + local function submit() + if submitted then return end + submitted = true + local lines = vim.api.nvim_buf_get_lines(win.buf, 0, -1, false) + win:close() + local text = vim.trim(table.concat(lines, '\n')) + on_submit(text) + end + + vim.keymap.set('n', '', submit, { buffer = win.buf, silent = true }) + vim.keymap.set('n', 'q', function() + if not submitted then + win:close() + end + end, { buffer = win.buf, silent = true }) + vim.cmd('startinsert') + else + vim.notify('[GoCodeReview]: guihua.lua not found, failed to create input window', vim.log.levels.WARN) + end + end + create_cmd('GoCodeReview', function(args) -- Use MCP-enhanced review when available local go_cfg = require('go').config() or {} - if go_cfg.mcp and go_cfg.mcp.enable then - local opts = parse_review_args(args) - require('go.mcp.review').review(opts) + + local function do_review(extra_opts) + if go_cfg.mcp and go_cfg.mcp.enable then + local opts = parse_review_args(args) + if extra_opts then opts = vim.tbl_extend('force', opts, extra_opts) end + require('go.mcp.review').review(opts) + else + if extra_opts and extra_opts.message then + -- Inject -m args so ai.code_review sees them + args.fargs = args.fargs or {} + table.insert(args.fargs, '-m') + table.insert(args.fargs, extra_opts.message) + end + require('go.ai').code_review(args) + end + end + + -- Check if -m is present with no text after it + local fargs = args.fargs or {} + local has_m, m_has_text = false, false + for idx, a in ipairs(fargs) do + if a == '-m' or a == '--message' then + has_m = true + m_has_text = (fargs[idx + 1] ~= nil and not fargs[idx + 1]:match('^%-')) + break + end + end + + if has_m and not m_has_text then + -- Open interactive buffer for multi-line input + open_message_input(function(text) + if text == '' then + vim.notify('[GoCodeReview]: cancelled (empty message)', vim.log.levels.INFO) + return + end + do_review({ message = text }) + end) else - -- Fallback: ai.code_review does its own arg parsing from raw command opts - require('go.ai').code_review(args) + do_review() end end, { nargs = '*', range = true, complete = function(_, _, _) - return { '-d', '--diff', '-b', '--brief', '-f', '--full' } + return { '-d', '--diff', '-b', '--brief', '-f', '--full', '-m', '--message' } end, }) diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index 75f65315d..cfcb9f9a2 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -56,6 +56,9 @@ Review the unified diff for bugs, correctness, error handling, concurrency, and ---@return string the user prompt for AI local function build_enriched_prompt(code_text, semantic_context, opts) local parts = {} + if opts and opts.message and opts.message ~= '' then + table.insert(parts, '## Change Description\n' .. opts.message .. '\n') + end if opts and opts.diff then table.insert(parts, '## Git Diff\n```diff\n' .. code_text .. '\n```') else From af173ed77ebfb150f16ad2754fb0c540b934af2e Mon Sep 17 00:00:00 2001 From: ray-x Date: Sat, 14 Mar 2026 21:54:15 +1100 Subject: [PATCH 13/21] update doc for the new args --- README.md | 33 +++++++++++++++++++++++++++++++++ doc/go.txt | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c43812634..ca1776aed 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ The plugin covers most features required for a gopher. - `GoCmtAI` β€” generate doc comments with AI for the declaration at cursor - `GoDocAI` β€” AI-powered documentation: find symbols by vague name and generate rich docs from source - `GoCodeReview` β€” AI code review for files, selections, or diffs; results populate the quickfix list +- `GoAIChat` β€” ask questions about Go code with AI; auto-includes function context and LSP references **Documentation & Navigation** @@ -784,9 +785,41 @@ actionable findings (errors, warnings, suggestions). | GoCodeReview -d develop | Review only changes (diff) against a specific branch | | GoCodeReview -b | Review with a brief/compact prompt (saves tokens) | | GoCodeReview -d -b | Diff review with brief prompt | +| GoCodeReview -m {text} | Provide change description for context-aware review | +| GoCodeReview -m | Open interactive editor for multi-line change description | + +The `-m` flag lets you describe what the changes are about so the reviewer can give more targeted feedback: + +```vim +:GoCodeReview -m add lru cache to search, remove fifo cache +:GoCodeReview -d -m refactor error handling for retries +:GoCodeReview -m " opens a floating editor for multi-line input +``` + +Literal `\n` in the message text is converted to newlines. When `-m` is used without text, a +floating editor (guihua.textview) opens for multi-line input β€” submit with ``, cancel with `q`. Requires `ai = { enable = true }` in your go.nvim setup. Results are loaded into the quickfix list. +### AI Chat + +`GoAIChat` lets you ask questions about Go code with AI. It automatically includes code context: + +- **Visual selection**: selected code is sent as context +- **Cursor in function**: the enclosing function text and LSP references/callers are included +- **No context**: opens an interactive prompt + +| Command | Description | +| -------------------------------- | ------------------------------------------------ | +| :'<,'>GoAIChat explain this code | Explain visually selected code | +| GoAIChat check for bugs | Check enclosing function for bugs | +| GoAIChat refactor this code | Suggest refactoring for the function under cursor | +| GoAIChat | Open interactive prompt | +| GoAIChat create a commit summary | Summarize git diff as a commit message | + +Tab completion provides common prompts: `explain this code`, `refactor this code`, +`check for bugs`, `check concurrency safety`, `suggest improvements`, etc. + ### AI Documentation `GoDocAI` finds a Go symbol by vague/partial name and generates rich AI documentation from its source. diff --git a/doc/go.txt b/doc/go.txt index 1a9e8fc14..ef31b2003 100644 --- a/doc/go.txt +++ b/doc/go.txt @@ -562,7 +562,7 @@ AI-POWERED ~ < Configure via the |ai| option. -:GoCodeReview [-d [{branch}]] [-b] *:GoCodeReview* +:GoCodeReview [-d [{branch}]] [-b] [-m [{message}]] *:GoCodeReview* Review Go code with AI and populate the quickfix list with actionable findings (errors, warnings, suggestions). Uses the configured AI provider (see |ai| option). @@ -574,6 +574,14 @@ AI-POWERED ~ -b, --brief Use a compact prompt that focuses on bugs, correctness, and major issues (saves tokens). + -m [{message}] Provide a change description so the + reviewer can give context-aware feedback. + If text follows -m, it is used directly. + Literal \n in the text is converted to + real newlines. + If -m is given alone, a floating editor + (guihua.textview) opens for multi-line + input. Submit with , cancel with q. Modes: :GoCodeReview Review the entire current file. @@ -585,10 +593,38 @@ AI-POWERED ~ Review changes against a specific branch. :GoCodeReview -b Review with brief/compact prompt. + :GoCodeReview -m add lru cache, remove fifo + Provide inline change description. + :GoCodeReview -m Open floating editor for multi-line + change description. + :GoCodeReview -d -m refactor error handling + Diff review with change context. Findings are parsed into vim quickfix format with severity: error (E), warning (W), or suggestion (I). +:GoAIChat [{question}] *:GoAIChat* + Ask questions about Go code with AI assistance. The response + is displayed in a floating window. + + Code context is automatically included: + - With a visual selection: the selected code is sent. + - Cursor inside a function: the enclosing function text and + LSP references/callers are gathered and sent. + - Otherwise: no code context (general question). + + Tab completion provides common prompts. + Requires `ai = { enable = true }` (see |ai| option). + + Examples: > + :GoAIChat explain this code + :'<,'>GoAIChat check for bugs + :GoAIChat refactor this code + :GoAIChat check concurrency safety + :GoAIChat suggest improvements + :GoAIChat create a commit summary +< + :GoDocAI [{query}] *:GoDocAI* Find a Go function, type, or symbol by a vague or partial name and generate rich AI documentation from its source code. From b569bdb307b182d319428d541df0a98f44ad0bbe Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:09:08 +1100 Subject: [PATCH 14/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/mcp/review.lua | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index cfcb9f9a2..3875c591d 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -84,10 +84,29 @@ function M.review(opts) -- Get the diff or file content (reuse existing logic) local code_text if opts.diff then - local branch = opts.branch or 'master' + -- Determine base branch for diff: + -- 1) honor explicit opts.branch + -- 2) otherwise, use ai.detect_default_branch() if available + -- 3) fall back to legacy master/main behavior for compatibility + local branch = opts.branch + if not branch or branch == '' then + if ai.detect_default_branch then + branch = ai.detect_default_branch() + end + end + + if not branch or branch == '' then + branch = 'master' + end + code_text = vim.fn.system({ 'git', 'diff', '-U10', branch, '--', '*.go' }) - if vim.v.shell_error ~= 0 then - code_text = vim.fn.system({ 'git', 'diff', '-U10', 'main', '--', '*.go' }) + + -- If diffing against the detected/explicit branch fails, try legacy fallbacks. + if vim.v.shell_error ~= 0 and branch ~= 'master' and branch ~= 'main' then + code_text = vim.fn.system({ 'git', 'diff', '-U10', 'master', '--', '*.go' }) + if vim.v.shell_error ~= 0 then + code_text = vim.fn.system({ 'git', 'diff', '-U10', 'main', '--', '*.go' }) + end end elseif opts.visual and opts.lines then code_text = opts.lines From 2209e338859afac4cd15fb5478a3ec7a553505c3 Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:09:19 +1100 Subject: [PATCH 15/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/ai.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/go/ai.lua b/lua/go/ai.lua index f352be718..8cdfdcb5f 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -540,7 +540,7 @@ end -- ─── GoCodeReview ──────────────────────────────────────────────────────────── ---- Detect the default branch ofnd. +--- Detect the default branch of the repository. --- @return string local function detect_default_branch() -- Check remote HEAD first (most reliable) From 27cdcf54f87e2e8e93f7894467344de9de2f249b Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:09:38 +1100 Subject: [PATCH 16/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/comment.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/go/comment.lua b/lua/go/comment.lua index aa801b971..0f9775398 100644 --- a/lua/go/comment.lua +++ b/lua/go/comment.lua @@ -114,7 +114,7 @@ local function get_declaration_at_cursor() for _, g in ipairs(getters) do local ns = g.fn() - if ns ~= nil and ns ~= {} and ns.declaring_node then + if ns and ns.declaring_node then local source = vim.treesitter.get_node_text(ns.declaring_node, bufnr) return ns, source, g.kind end From 00902545c60f029750879f2e95b0bd64f1b7ee89 Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:09:48 +1100 Subject: [PATCH 17/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca1776aed..5551b3ceb 100644 --- a/README.md +++ b/README.md @@ -963,7 +963,7 @@ require('go').setup({ -- go_input = require('guihua.input').input -- set to vim.ui.input to disable guihua input -- go_select = require('guihua.select').select -- vim.ui.select to disable guihua select lsp_document_formatting = true, - -- set to true: use gopls to forβˆ‚mat + -- set to true: use gopls to format -- false if you want to use other formatter tool(e.g. efm, nulls) lsp_inlay_hints = { enable = true, -- this is the only field apply to neovim > 0.10 From 5385b698dc504d2fa92958b158f55be2ddf88e3c Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:10:12 +1100 Subject: [PATCH 18/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/mcp/review.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/go/mcp/review.lua b/lua/go/mcp/review.lua index 3875c591d..387033e88 100644 --- a/lua/go/mcp/review.lua +++ b/lua/go/mcp/review.lua @@ -169,13 +169,13 @@ function M._handle_review_response(response) end local qf_items = {} + local severity_map = { + error = 'E', + warning = 'W', + info = 'I', + } log(findings) for _, item in ipairs(findings) do - local severity_map = { - error = 'E', - warning = 'W', - info = 'I', - } -- Build enriched message with violation/principle/refactor when available local parts = {} From 250f1f17849fdf8317b195dd67e220c29132eeb1 Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:10:39 +1100 Subject: [PATCH 19/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/ai.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/go/ai.lua b/lua/go/ai.lua index 8cdfdcb5f..e6169becd 100644 --- a/lua/go/ai.lua +++ b/lua/go/ai.lua @@ -1099,7 +1099,7 @@ function M.chat(opts) if question ~= '' then -- If we have function context, enrich with references - if func_name and func_name ~= '' and not code:find('create a commit') then + if func_name and func_name ~= '' and code and not code:find('create a commit') then dispatch_with_refs(question) else dispatch(question) From 9577fb4f7cd8dd6e980d91577152019bba494904 Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:10:57 +1100 Subject: [PATCH 20/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/mcp/client.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lua/go/mcp/client.lua b/lua/go/mcp/client.lua index a5b7610e3..5ec81dd04 100644 --- a/lua/go/mcp/client.lua +++ b/lua/go/mcp/client.lua @@ -152,6 +152,9 @@ function McpClient:shutdown() if self.handle then self.stdin:close() self.stdout:close() + if self.stderr then + self.stderr:close() + end self.handle:kill('sigterm') end end From a5db19dae98f95202b9977a2de3f76ed32f20a34 Mon Sep 17 00:00:00 2001 From: rayx Date: Sat, 14 Mar 2026 22:11:04 +1100 Subject: [PATCH 21/21] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lua/go/mcp/init.lua | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lua/go/mcp/init.lua b/lua/go/mcp/init.lua index 0efbbc970..d6b98fab6 100644 --- a/lua/go/mcp/init.lua +++ b/lua/go/mcp/init.lua @@ -1,4 +1,10 @@ -local M = { _client = nil } +local M = { + _client = nil, + _config = { + gopls_cmd = { 'gopls', 'mcp' }, + root_dir = nil, + }, +} local client_mod = require('go.mcp.client') function M.setup(opts)