Skip to content

Commit cf573e7

Browse files
committed
feat: automatically configure gopls GOROOT
1 parent a6ee6b7 commit cf573e7

File tree

5 files changed

+214
-9
lines changed

5 files changed

+214
-9
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ please.nvim is a plugin which allows you interact with your Please repository fr
1010
* Yank a target's label with `please.yank()`.
1111
* `please` configured as the `filetype` for `BUILD`, `BUILD.plz`, `*.build`, and `*.build_defs`
1212
files.
13-
* `please` LSP client configured to use `plz tool lps` for `please` files
1413
* `ini` configured as the `filetype` for `.plzconfig` files to enable better syntax highlighting.
14+
* `please` LSP client configured to use `plz tool lps` for `please` files.
15+
* [gopls](https://go.dev/gopls) gopls language server configured to use appropriate `GOROOT` when
16+
started in a Please respository.
1517
* Python tree-sitter parser configured to be used for please files to enable better syntax
1618
highlighting and use of all tree-sitter features in build files.
1719

doc/please.txt

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ Features ~
2020
• Yank a target's label with |please.yank()|.
2121
`please` configured as the 'filetype' for `BUILD`, `BUILD.plz`, `*.build`,
2222
and `*.build_defs` files.
23-
`please` LSP client configured to use `plz tool lps` for `please` files.
24-
See |please-lsp|.
2523
`ini` configured as the 'filetype' for `.plzconfig` files to enable better
2624
syntax highlighting.
25+
`please` LSP client configured to use `plz tool lps` for `please` files.
26+
See |please-lsp-client|.
27+
• gopls language server configured to use appropriate `GOROOT` when started
28+
in a Please respository. See |please-gopls-GOROOT|.
2729
• Python tree-sitter parser configured to be used for please files to enable
2830
better syntax highlighting and use of all tree-sitter features in build
2931
files.
@@ -60,12 +62,30 @@ appearance of them to your taste. See |vim.ui| and the fantastic
6062
https://github.com/stevearc/dressing.nvim for more information.
6163

6264
==============================================================================
63-
LSP *please-lsp*
65+
LSP CONFIGURATION *please-lsp-config*
6466

67+
*please-lsp-client*
68+
Please LSP Client ~
6569
please.nvim configures the `please` LSP client to use `plz tool lps` for
6670
`please` files using |vim.lsp.config()|. Call |vim.lsp.enable| to enable it:
6771
>lua
6872
vim.lsp.enable('please')
73+
<
74+
*please-gopls-GOROOT*
75+
Gopls GOROOT ~
76+
The gopls language server uses the `GOROOT` environment variable to determine
77+
where the standard library is located. The go-rules Please plugin allows
78+
configuring the version of Go to use via the `GoTool` `.plzconfig` setting.
79+
80+
please.nvim configures `gopls` using |vim.lsp.config()| to use the appropriate
81+
`GOROOT` when started in a Please repository, based on the `GoTool` setting.
82+
This means that operations like jumping to a definition or listing references
83+
with standard library modules will refer to the correct location for the
84+
version of Go defined by the repository.
85+
86+
See also: ~
87+
https://go.dev/gopls
88+
https://github.com/please-build/go-rules
6989

7090
==============================================================================
7191
SETUP *please-setup*

lua/please/logging.lua

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,16 @@ function M.toggle_debug()
88
return debug_enabled
99
end
1010

11-
local function log(msg, level, ...)
11+
---@param msg string
12+
---@param ... string
13+
---@return string
14+
function M.format(msg, ...)
1215
local formatted_msg = string.format(msg, ...)
13-
formatted_msg = string.format('[please.nvim]: %s', formatted_msg)
16+
return string.format('[please.nvim]: %s', formatted_msg)
17+
end
18+
19+
local function log(msg, level, ...)
20+
local formatted_msg = M.format(msg, ...)
1421
if vim.in_fast_event() then
1522
vim.schedule(function()
1623
vim.notify(formatted_msg, level)

lua/please/query.lua

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
local logging = require('please.logging')
22
local plz = require('please.plz')
3+
local runner = require('please.runner')
34

45
local M = {}
56

@@ -111,4 +112,110 @@ function M.output(root, target)
111112
return output
112113
end
113114

115+
---Determines the appropriate GOROOT for a repo and passes it to the given callback.
116+
---The result is passed to a callback because a target may need to be built to create the GOROOT. Progress will be shown
117+
---in a floating window in this case.
118+
---Determining the GOROOT may fail. In this case, the callback will be passed `nil`, `errmsg`.
119+
---@param root string absolute path to the repo root
120+
---@param cb fun(goroot:string?, errmsg:string?) function called on success or error
121+
function M.with_goroot(root, cb)
122+
logging.log_call('query.go_root')
123+
124+
local gotool = 'go'
125+
local gotools, err = M.config(root, 'plugin.go.gotool')
126+
if gotools then
127+
gotool = gotools[1]
128+
elseif not (err or ''):match('Settable field not defined') then
129+
cb(nil, string.format('determining GOROOT: %s', err))
130+
return
131+
end
132+
133+
if vim.startswith(gotool, ':') or vim.startswith(gotool, '//') then
134+
gotool = gotool:gsub('|go$', '')
135+
local gotool_output, err = M.output(root, gotool)
136+
if not gotool_output then
137+
cb(nil, string.format('determining GOROOT: %s', gotool, err))
138+
return
139+
end
140+
local rel_goroot = vim.trim(gotool_output)
141+
local goroot = vim.fs.joinpath(root, rel_goroot)
142+
if not vim.uv.fs_stat(goroot) then
143+
local msg = logging.format(
144+
'GOROOT "%s" for repository "%s" does not exist. Build plugin.go.gotool target "%s" to create it?',
145+
rel_goroot,
146+
root,
147+
gotool
148+
)
149+
local ok, result = pcall(vim.fn.confirm, msg, '&Yes\n&No', 1, 'Question')
150+
if not ok and result ~= 'Keyboard interrupt' then
151+
error(result)
152+
end
153+
local build = ok and result == 1
154+
if not build then
155+
cb(nil, string.format('determining GOROOT: GOROOT "%s" for repository "%s" does not exist', rel_goroot, root))
156+
return
157+
end
158+
runner.Runner.start(root, { 'build', gotool }, {
159+
on_exit = function(success, runner)
160+
if success then
161+
runner:minimise()
162+
logging.info('built plugin.go.gotool target "%s" successfully', gotool)
163+
cb(goroot)
164+
else
165+
cb(nil, string.format('determining GOROOT: building plugin.go.gotool target "%s" failed', gotool))
166+
end
167+
end,
168+
})
169+
return
170+
end
171+
cb(goroot)
172+
return
173+
end
174+
175+
if vim.startswith(gotool, '/') then
176+
if not vim.uv.fs_stat(gotool) then
177+
cb(nil, string.format('determining GOROOT: plugin.go.gotool "%s" does not exist', gotool))
178+
return
179+
end
180+
local goroot_res = vim.system({ gotool, 'env', 'GOROOT' }):wait()
181+
if goroot_res.code == 0 then
182+
cb(vim.trim(goroot_res.stdout))
183+
return
184+
else
185+
cb(nil, string.format('determining GOROOT: %s env GOROOT: %s', gotool, goroot_res.stderr))
186+
return
187+
end
188+
end
189+
190+
local build_paths, err = M.config(root, 'build.path')
191+
if not build_paths then
192+
cb(nil, string.format('determining GOROOT: %s', err))
193+
return
194+
end
195+
for _, build_path in ipairs(build_paths) do
196+
for path in vim.gsplit(build_path, ':') do
197+
local go = vim.fs.joinpath(path, gotool)
198+
if vim.uv.fs_stat(go) then
199+
local goroot_res = vim.system({ go, 'env', 'GOROOT' }):wait()
200+
if goroot_res.code == 0 then
201+
cb(vim.trim(goroot_res.stdout))
202+
return
203+
else
204+
cb(nil, string.format('determining GOROOT: %s env GOROOT: %s', go, goroot_res.stderr))
205+
return
206+
end
207+
end
208+
end
209+
end
210+
211+
cb(
212+
nil,
213+
string.format(
214+
'determining GOROOT: plugin.go.gotool "%s" not found in build.path "%s"',
215+
gotool,
216+
table.concat(build_paths, ':')
217+
)
218+
)
219+
end
220+
114221
return M

plugin/please.lua

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,21 @@ if vim.g.loaded_please then
88
return
99
end
1010

11-
local require_on_index = require
12-
1311
---@param modname string
1412
---@return unknown
15-
function require_on_index(modname)
13+
local function require_on_index(modname)
1614
return setmetatable({}, {
1715
__index = function(_, k)
1816
return require(modname)[k]
1917
end,
2018
})
2119
end
2220

21+
---@module 'please'
2322
local please = require_on_index('please')
23+
---@module 'please.query'
24+
local query = require_on_index('please.query')
25+
---@module 'please.logging'
2426
local logging = require_on_index('please.logging')
2527

2628
vim.filetype.add({
@@ -51,6 +53,73 @@ vim.lsp.config('please', {
5153
workspace_required = true,
5254
})
5355

56+
---@type table<integer, string>
57+
local buf_goroots = {}
58+
59+
vim.api.nvim_create_autocmd('VimEnter', {
60+
group = vim.api.nvim_create_augroup('please.nvim_gopls_config', {}),
61+
desc = 'Configure gopls language server to use appropriate GOROOT when started in a Please repository',
62+
callback = function()
63+
if not vim.lsp.config.gopls then
64+
return
65+
end
66+
local original_cmd = vim.lsp.config.gopls.cmd
67+
if type(original_cmd) ~= 'table' then
68+
return
69+
end
70+
local original_root_dir = vim.lsp.config.gopls.root_dir
71+
if not original_root_dir then
72+
return
73+
end
74+
---@type vim.lsp.Config
75+
local config = {
76+
root_dir = function(bufnr, cb)
77+
local filename = vim.api.nvim_buf_get_name(bufnr)
78+
local plz_root = vim.fs.root(filename, '.plzconfig')
79+
if not plz_root then
80+
if type(original_root_dir) == 'string' then
81+
cb(original_root_dir)
82+
else
83+
original_root_dir(bufnr, cb)
84+
end
85+
return
86+
end
87+
-- If this call is not scheduled, two FileType events fire causing root_dir to be called twice. Not sure why...
88+
vim.schedule(function()
89+
query.with_goroot(plz_root, function(goroot, err)
90+
if goroot then
91+
buf_goroots[bufnr] = goroot
92+
else
93+
logging.warn('starting gopls in repository "%s": %s', plz_root, err)
94+
end
95+
if type(original_root_dir) == 'string' then
96+
cb(original_root_dir)
97+
else
98+
original_root_dir(bufnr, cb)
99+
end
100+
end)
101+
end)
102+
end,
103+
cmd = function(dispatchers)
104+
local config = vim.lsp.config.gopls
105+
local bufnr = vim.api.nvim_get_current_buf()
106+
local goroot = buf_goroots[bufnr]
107+
if goroot then
108+
config = vim.deepcopy(config)
109+
config.cmd_env = config.cmd_env or {}
110+
config.cmd_env.GOROOT = goroot
111+
end
112+
return vim.lsp.rpc.start(original_cmd, dispatchers, {
113+
cwd = config.cmd_cwd,
114+
env = config.cmd_env,
115+
detached = config.detached,
116+
})
117+
end,
118+
}
119+
vim.lsp.config('gopls', config)
120+
end,
121+
})
122+
54123
---Returns all candidates which start with the prefix, sorted.
55124
---@param prefix string
56125
---@param candidates string[]

0 commit comments

Comments
 (0)