1515package buflsp
1616
1717import (
18+ "bytes"
19+ "context"
20+ "fmt"
21+ "os"
1822 "path/filepath"
1923 "sync"
2024
25+ "buf.build/go/standard/xos/xexec"
2126 "github.com/bufbuild/buf/private/bufpkg/bufconfig"
2227 "github.com/bufbuild/buf/private/bufpkg/bufparse"
28+ "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginref"
2329 "go.lsp.dev/protocol"
2430 "gopkg.in/yaml.v3"
2531)
2632
33+ const (
34+ CommandRunGenerate = "buf.generate.run"
35+ CommandCheckPluginUpdates = "buf.generate.checkPluginUpdates"
36+ )
37+
2738// isBufGenYAMLURI reports whether uri refers to a buf.gen.yaml file.
2839func isBufGenYAMLURI (uri protocol.URI ) bool {
2940 return filepath .Base (uri .Filename ()) == bufconfig .DefaultBufGenYAMLFileName
3041}
3142
3243// bufGenYAMLManager tracks open buf.gen.yaml files in the LSP session.
3344type bufGenYAMLManager struct {
45+ lsp * lsp
3446 mu sync.Mutex
3547 uriToFile map [protocol.URI ]* bufGenYAMLFile
3648}
3749
38- func newBufGenYAMLManager () * bufGenYAMLManager {
50+ func newBufGenYAMLManager (lsp * lsp ) * bufGenYAMLManager {
3951 return & bufGenYAMLManager {
52+ lsp : lsp ,
4053 uriToFile : make (map [protocol.URI ]* bufGenYAMLFile ),
4154 }
4255}
4356
4457// bufGenYAMLFile holds the parsed state of an open buf.gen.yaml file.
4558type bufGenYAMLFile struct {
46- docNode * yaml.Node // parsed YAML document node, nil if parse failed
47- refs []bsrRef // plugins[*].remote and inputs[*].module BSR references
59+ docNode * yaml.Node // parsed YAML document node, nil if parse failed
60+ refs []bsrRef // plugins[*].remote and inputs[*].module BSR references
61+ versionedPluginRefs []bsrRef // plugins[*].remote with an explicit version (for update checks)
62+ pluginsKeyLine uint32 // 0-indexed line of the "plugins:" key
4863}
4964
5065// Track opens or refreshes a buf.gen.yaml file.
5166func (m * bufGenYAMLManager ) Track (uri protocol.URI , text string ) {
5267 normalized := normalizeURI (uri )
5368 docNode := parseYAMLDoc (text )
69+ allRefs , versionedPluginRefs , pluginsKeyLine := parseBufGenYAMLRefs (docNode )
5470 f := & bufGenYAMLFile {
55- docNode : docNode ,
56- refs : parseBufGenYAMLRefs (docNode ),
71+ docNode : docNode ,
72+ refs : allRefs ,
73+ versionedPluginRefs : versionedPluginRefs ,
74+ pluginsKeyLine : pluginsKeyLine ,
5775 }
5876 m .mu .Lock ()
5977 defer m .mu .Unlock ()
6078 m .uriToFile [normalized ] = f
6179}
6280
63- // Close stops tracking a buf.gen.yaml file.
64- func (m * bufGenYAMLManager ) Close (uri protocol.URI ) {
81+ // Close stops tracking a buf.gen.yaml file and clears any diagnostics it published.
82+ func (m * bufGenYAMLManager ) Close (ctx context.Context , uri protocol.URI ) {
83+ normalized := normalizeURI (uri )
6584 m .mu .Lock ()
66- delete (m .uriToFile , normalizeURI ( uri ) )
85+ delete (m .uriToFile , normalized )
6786 m .mu .Unlock ()
87+ publishDiagnostics (ctx , m .lsp .client , normalized , nil )
6888}
6989
7090// GetHover returns hover documentation for the buf.gen.yaml field at the given
@@ -107,20 +127,125 @@ func (m *bufGenYAMLManager) GetDocumentLinks(uri protocol.URI) []protocol.Docume
107127 return links
108128}
109129
110- // parseBufGenYAMLRefs walks the parsed buf.gen.yaml document and collects all
111- // BSR references: plugins[*].remote and inputs[*].module scalar values with
112- // their source positions, in document order.
113- //
114- // Returns nil if doc is nil or not a valid document.
115- func parseBufGenYAMLRefs (doc * yaml.Node ) []bsrRef {
116- if doc == nil || doc .Kind != yaml .DocumentNode || len (doc .Content ) == 0 {
130+ // GetCodeLenses returns code lenses for the given buf.gen.yaml URI.
131+ func (m * bufGenYAMLManager ) GetCodeLenses (uri protocol.URI ) []protocol.CodeLens {
132+ m .mu .Lock ()
133+ f , ok := m .uriToFile [normalizeURI (uri )]
134+ m .mu .Unlock ()
135+ if ! ok {
117136 return nil
118137 }
138+ lenses := []protocol.CodeLens {
139+ {
140+ Range : protocol.Range {},
141+ Command : & protocol.Command {
142+ Title : "Run buf generate" ,
143+ Command : CommandRunGenerate ,
144+ Arguments : []any {string (uri )},
145+ },
146+ },
147+ }
148+ if len (f .versionedPluginRefs ) > 0 {
149+ pluginsRange := protocol.Range {
150+ Start : protocol.Position {Line : f .pluginsKeyLine },
151+ End : protocol.Position {Line : f .pluginsKeyLine },
152+ }
153+ lenses = append (lenses , protocol.CodeLens {
154+ Range : pluginsRange ,
155+ Command : & protocol.Command {
156+ Title : "Check for plugin updates" ,
157+ Command : CommandCheckPluginUpdates ,
158+ Arguments : []any {string (uri )},
159+ },
160+ })
161+ }
162+ return lenses
163+ }
164+
165+ // ExecuteRunGenerate runs buf generate in the directory containing the given
166+ // buf.gen.yaml URI. Results are reported to the user via ShowMessage.
167+ func (m * bufGenYAMLManager ) ExecuteRunGenerate (ctx context.Context , uri protocol.URI ) error {
168+ dirPath := filepath .Dir (uri .Filename ())
169+ executable , err := os .Executable ()
170+ if err != nil {
171+ executable = "buf"
172+ }
173+ msgType := protocol .MessageTypeInfo
174+ msg := "buf generate completed successfully"
175+ var outBuf bytes.Buffer
176+ if err := xexec .Run (ctx , executable ,
177+ xexec .WithArgs ("generate" ),
178+ xexec .WithDir (dirPath ),
179+ xexec .WithStdout (& outBuf ),
180+ xexec .WithStderr (& outBuf ),
181+ ); err != nil {
182+ msgType = protocol .MessageTypeError
183+ msg = fmt .Sprintf ("buf generate failed:\n %s" , outBuf .String ())
184+ }
185+ _ = m .lsp .client .ShowMessage (ctx , & protocol.ShowMessageParams {
186+ Type : msgType ,
187+ Message : msg ,
188+ })
189+ return nil
190+ }
191+
192+ // ExecuteCheckPluginUpdates queries the BSR for the latest version of each
193+ // versioned remote plugin in the buf.gen.yaml file and publishes an
194+ // informational diagnostic on any plugin line where a newer version is
195+ // available. It does not modify any files.
196+ func (m * bufGenYAMLManager ) ExecuteCheckPluginUpdates (ctx context.Context , uri protocol.URI ) error {
197+ normalized := normalizeURI (uri )
198+ m .mu .Lock ()
199+ f , ok := m .uriToFile [normalized ]
200+ m .mu .Unlock ()
201+ if ! ok || len (f .versionedPluginRefs ) == 0 {
202+ publishDiagnostics (ctx , m .lsp .client , normalized , nil )
203+ return nil
204+ }
205+
206+ var diagnostics []protocol.Diagnostic
207+ for _ , entry := range f .versionedPluginRefs {
208+ identity , pinnedVersion , err := bufremotepluginref .ParsePluginIdentityOptionalVersion (entry .ref )
209+ if err != nil || pinnedVersion == "" {
210+ continue
211+ }
212+ latestVersion , err := m .lsp .curatedPluginVersionProvider .GetLatestVersion (
213+ ctx , identity .Remote (), identity .Owner (), identity .Plugin (),
214+ )
215+ if err != nil {
216+ return fmt .Errorf ("resolving latest version for %s: %w" , identity .IdentityString (), err )
217+ }
218+ if latestVersion == "" || latestVersion == pinnedVersion {
219+ continue
220+ }
221+ diagnostics = append (diagnostics , protocol.Diagnostic {
222+ Range : entry .refRange ,
223+ Severity : protocol .DiagnosticSeverityInformation ,
224+ Source : serverName ,
225+ Message : fmt .Sprintf (
226+ "%s can be updated (latest: %s)" ,
227+ identity .IdentityString (),
228+ latestVersion ,
229+ ),
230+ })
231+ }
232+ publishDiagnostics (ctx , m .lsp .client , normalized , diagnostics )
233+ return nil
234+ }
235+
236+ // parseBufGenYAMLRefs walks the parsed buf.gen.yaml document and collects BSR
237+ // references in document order: plugins[*].remote and inputs[*].module scalar
238+ // values with their source positions.
239+ func parseBufGenYAMLRefs (doc * yaml.Node ) ([]bsrRef , []bsrRef , uint32 ) {
240+ if doc == nil || doc .Kind != yaml .DocumentNode || len (doc .Content ) == 0 {
241+ return nil , nil , 0
242+ }
119243 mapping := doc .Content [0 ]
120244 if mapping .Kind != yaml .MappingNode {
121- return nil
245+ return nil , nil , 0
122246 }
123- var refs []bsrRef
247+ var refs , versionedPluginRefs []bsrRef
248+ var pluginsKeyLine uint32
124249 for i := 0 ; i + 1 < len (mapping .Content ); i += 2 {
125250 keyNode := mapping .Content [i ]
126251 valNode := mapping .Content [i + 1 ]
@@ -129,14 +254,19 @@ func parseBufGenYAMLRefs(doc *yaml.Node) []bsrRef {
129254 if valNode .Kind != yaml .SequenceNode {
130255 continue
131256 }
257+ pluginsKeyLine = uint32 (keyNode .Line - 1 ) //nolint:gosec // yaml.Node.Line is 1-indexed and always ≥ 1
132258 for _ , item := range valNode .Content {
133259 if item .Kind != yaml .MappingNode {
134260 continue
135261 }
136262 for j := 0 ; j + 1 < len (item .Content ); j += 2 {
137263 k , v := item .Content [j ], item .Content [j + 1 ]
138264 if k .Value == "remote" && v .Kind == yaml .ScalarNode && v .Value != "" {
139- refs = append (refs , bsrRef {ref : v .Value , refRange : yamlNodeRange (v )})
265+ entry := bsrRef {ref : v .Value , refRange : yamlNodeRange (v )}
266+ refs = append (refs , entry )
267+ if _ , version , err := bufremotepluginref .ParsePluginIdentityOptionalVersion (v .Value ); err == nil && version != "" {
268+ versionedPluginRefs = append (versionedPluginRefs , entry )
269+ }
140270 }
141271 }
142272 }
@@ -157,5 +287,5 @@ func parseBufGenYAMLRefs(doc *yaml.Node) []bsrRef {
157287 }
158288 }
159289 }
160- return refs
290+ return refs , versionedPluginRefs , pluginsKeyLine
161291}
0 commit comments