diff --git a/.golangci.yml b/.golangci.yml index bd0974cfb3..83c61d7872 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -208,6 +208,11 @@ linters: # G115 checks for use of truncating conversions. path: private/buf/buflsp/buf_yaml_hover.go text: "G115:" + - linters: + - gosec + # G115 checks for use of truncating conversions. + path: private/buf/buflsp/buf_gen_yaml.go + text: "G115:" - linters: - containedctx # Type must implement an interface whose methods do not accept context. But this diff --git a/CHANGELOG.md b/CHANGELOG.md index f30585a931..b3a193a233 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Fix LSP incorrectly reporting "edition '2024' not yet fully supported" errors. - Fix CEL compilation error messages in `buf lint` to use the structured error API instead of parsing cel-go's text output. - Add `--debug-address` flag to `buf lsp serve` to provide debug and profile support. +- Add LSP code lenses for `buf.gen.yaml` files: "Run buf generate" (always shown at line 0) and "Check for plugin updates" (shown at the `plugins:` key line when versioned remote plugins are present). ## [v1.68.1] - 2026-04-14 diff --git a/cmd/buf/internal/command/lsp/lspserve/lspserve.go b/cmd/buf/internal/command/lsp/lspserve/lspserve.go index 60dabac67b..c7fb744c50 100644 --- a/cmd/buf/internal/command/lsp/lspserve/lspserve.go +++ b/cmd/buf/internal/command/lsp/lspserve/lspserve.go @@ -28,8 +28,12 @@ import ( "buf.build/go/app/appcmd" "buf.build/go/app/appext" "buf.build/go/standard/xio" + "connectrpc.com/connect" "github.com/bufbuild/buf/private/buf/bufcli" "github.com/bufbuild/buf/private/buf/buflsp" + "github.com/bufbuild/buf/private/gen/proto/connect/buf/alpha/registry/v1alpha1/registryv1alpha1connect" + registryv1alpha1 "github.com/bufbuild/buf/private/gen/proto/go/buf/alpha/registry/v1alpha1" + "github.com/bufbuild/buf/private/pkg/connectclient" "github.com/bufbuild/protocompile/experimental/incremental" "github.com/spf13/pflag" "go.lsp.dev/jsonrpc2" @@ -152,6 +156,11 @@ func run( return err } + clientConfig, err := bufcli.NewConnectClientConfig(container) + if err != nil { + return err + } + conn, err := buflsp.Serve( ctx, bufcli.Version, @@ -163,6 +172,7 @@ func run( incremental.New(), moduleKeyProvider, graphProvider, + &lspCuratedPluginProvider{clientConfig: clientConfig}, ) if err != nil { return err @@ -171,6 +181,29 @@ func run( return conn.Err() } +// lspCuratedPluginProvider implements the curatedPluginVersionProvider interface +// required by buflsp.Serve using the BSR alpha plugin curation API. +type lspCuratedPluginProvider struct { + clientConfig *connectclient.Config +} + +func (p *lspCuratedPluginProvider) GetLatestVersion(ctx context.Context, registry, owner, plugin string) (string, error) { + client := connectclient.Make(p.clientConfig, registry, registryv1alpha1connect.NewPluginCurationServiceClient) + resp, err := client.GetLatestCuratedPlugin(ctx, connect.NewRequest( + registryv1alpha1.GetLatestCuratedPluginRequest_builder{ + Owner: owner, + Name: plugin, + }.Build(), + )) + if err != nil { + return "", err + } + if !resp.Msg.HasPlugin() { + return "", nil + } + return resp.Msg.GetPlugin().GetVersion(), nil +} + // dial opens a connection to the LSP client. func dial(container appext.Container, flags *flags) (io.ReadWriteCloser, error) { switch { diff --git a/private/buf/buflsp/buf_gen_yaml.go b/private/buf/buflsp/buf_gen_yaml.go index a9dc231970..2aacc31f69 100644 --- a/private/buf/buflsp/buf_gen_yaml.go +++ b/private/buf/buflsp/buf_gen_yaml.go @@ -15,15 +15,28 @@ package buflsp import ( + "bytes" + "context" + "fmt" + "os" "path/filepath" "sync" + "buf.build/go/standard/xos/xexec" "github.com/bufbuild/buf/private/bufpkg/bufconfig" "github.com/bufbuild/buf/private/bufpkg/bufparse" + "github.com/bufbuild/buf/private/bufpkg/bufremoteplugin/bufremotepluginref" "go.lsp.dev/protocol" "gopkg.in/yaml.v3" ) +// CommandRunGenerate is the LSP workspace command to run buf generate for a buf.gen.yaml file. +const CommandRunGenerate = "buf.generate.run" + +// CommandCheckPluginUpdates is the LSP workspace command to check for newer versions of remote +// plugins in a buf.gen.yaml file and publish informational diagnostics for any that are outdated. +const CommandCheckPluginUpdates = "buf.generate.checkPluginUpdates" + // isBufGenYAMLURI reports whether uri refers to a buf.gen.yaml file. func isBufGenYAMLURI(uri protocol.URI) bool { return filepath.Base(uri.Filename()) == bufconfig.DefaultBufGenYAMLFileName @@ -31,40 +44,49 @@ func isBufGenYAMLURI(uri protocol.URI) bool { // bufGenYAMLManager tracks open buf.gen.yaml files in the LSP session. type bufGenYAMLManager struct { + lsp *lsp mu sync.Mutex uriToFile map[protocol.URI]*bufGenYAMLFile } -func newBufGenYAMLManager() *bufGenYAMLManager { +func newBufGenYAMLManager(lsp *lsp) *bufGenYAMLManager { return &bufGenYAMLManager{ + lsp: lsp, uriToFile: make(map[protocol.URI]*bufGenYAMLFile), } } // bufGenYAMLFile holds the parsed state of an open buf.gen.yaml file. type bufGenYAMLFile struct { - docNode *yaml.Node // parsed YAML document node, nil if parse failed - refs []bsrRef // plugins[*].remote and inputs[*].module BSR references + docNode *yaml.Node // parsed YAML document node, nil if parse failed + refs []bsrRef // plugins[*].remote and inputs[*].module BSR references + versionedPluginRefs []bsrRef // plugins[*].remote with an explicit version (for update checks) + pluginsKeyLine uint32 // 0-indexed line of the "plugins:" key } // Track opens or refreshes a buf.gen.yaml file. func (m *bufGenYAMLManager) Track(uri protocol.URI, text string) { normalized := normalizeURI(uri) docNode := parseYAMLDoc(text) + allRefs, versionedPluginRefs, pluginsKeyLine := parseBufGenYAMLRefs(docNode) f := &bufGenYAMLFile{ - docNode: docNode, - refs: parseBufGenYAMLRefs(docNode), + docNode: docNode, + refs: allRefs, + versionedPluginRefs: versionedPluginRefs, + pluginsKeyLine: pluginsKeyLine, } m.mu.Lock() defer m.mu.Unlock() m.uriToFile[normalized] = f } -// Close stops tracking a buf.gen.yaml file. -func (m *bufGenYAMLManager) Close(uri protocol.URI) { +// Close stops tracking a buf.gen.yaml file and clears any diagnostics it published. +func (m *bufGenYAMLManager) Close(ctx context.Context, uri protocol.URI) { + normalized := normalizeURI(uri) m.mu.Lock() - delete(m.uriToFile, normalizeURI(uri)) + delete(m.uriToFile, normalized) m.mu.Unlock() + publishDiagnostics(ctx, m.lsp.client, normalized, nil) } // GetHover returns hover documentation for the buf.gen.yaml field at the given @@ -107,20 +129,125 @@ func (m *bufGenYAMLManager) GetDocumentLinks(uri protocol.URI) []protocol.Docume return links } -// parseBufGenYAMLRefs walks the parsed buf.gen.yaml document and collects all -// BSR references: plugins[*].remote and inputs[*].module scalar values with -// their source positions, in document order. -// -// Returns nil if doc is nil or not a valid document. -func parseBufGenYAMLRefs(doc *yaml.Node) []bsrRef { - if doc == nil || doc.Kind != yaml.DocumentNode || len(doc.Content) == 0 { +// GetCodeLenses returns code lenses for the given buf.gen.yaml URI. +func (m *bufGenYAMLManager) GetCodeLenses(uri protocol.URI) []protocol.CodeLens { + m.mu.Lock() + f, ok := m.uriToFile[normalizeURI(uri)] + m.mu.Unlock() + if !ok { return nil } + lenses := []protocol.CodeLens{ + { + Range: protocol.Range{}, + Command: &protocol.Command{ + Title: "Run buf generate", + Command: CommandRunGenerate, + Arguments: []any{string(uri)}, + }, + }, + } + if len(f.versionedPluginRefs) > 0 { + pluginsRange := protocol.Range{ + Start: protocol.Position{Line: f.pluginsKeyLine}, + End: protocol.Position{Line: f.pluginsKeyLine}, + } + lenses = append(lenses, protocol.CodeLens{ + Range: pluginsRange, + Command: &protocol.Command{ + Title: "Check for plugin updates", + Command: CommandCheckPluginUpdates, + Arguments: []any{string(uri)}, + }, + }) + } + return lenses +} + +// ExecuteRunGenerate runs buf generate in the directory containing the given +// buf.gen.yaml URI. Results are reported to the user via ShowMessage. +func (m *bufGenYAMLManager) ExecuteRunGenerate(ctx context.Context, uri protocol.URI) error { + dirPath := filepath.Dir(uri.Filename()) + executable, err := os.Executable() + if err != nil { + executable = "buf" + } + msgType := protocol.MessageTypeInfo + msg := "buf generate completed successfully" + var outBuf bytes.Buffer + if err := xexec.Run(ctx, executable, + xexec.WithArgs("generate"), + xexec.WithDir(dirPath), + xexec.WithStdout(&outBuf), + xexec.WithStderr(&outBuf), + ); err != nil { + msgType = protocol.MessageTypeError + msg = fmt.Sprintf("buf generate failed:\n%s", outBuf.String()) + } + _ = m.lsp.client.ShowMessage(ctx, &protocol.ShowMessageParams{ + Type: msgType, + Message: msg, + }) + return nil +} + +// ExecuteCheckPluginUpdates queries the BSR for the latest version of each +// versioned remote plugin in the buf.gen.yaml file and publishes an +// informational diagnostic on any plugin line where a newer version is +// available. It does not modify any files. +func (m *bufGenYAMLManager) ExecuteCheckPluginUpdates(ctx context.Context, uri protocol.URI) error { + normalized := normalizeURI(uri) + m.mu.Lock() + f, ok := m.uriToFile[normalized] + m.mu.Unlock() + if !ok || len(f.versionedPluginRefs) == 0 { + publishDiagnostics(ctx, m.lsp.client, normalized, nil) + return nil + } + + var diagnostics []protocol.Diagnostic + for _, entry := range f.versionedPluginRefs { + identity, pinnedVersion, err := bufremotepluginref.ParsePluginIdentityOptionalVersion(entry.ref) + if err != nil || pinnedVersion == "" { + continue + } + latestVersion, err := m.lsp.curatedPluginVersionProvider.GetLatestVersion( + ctx, identity.Remote(), identity.Owner(), identity.Plugin(), + ) + if err != nil { + return fmt.Errorf("resolving latest version for %s: %w", identity.IdentityString(), err) + } + if latestVersion == "" || latestVersion == pinnedVersion { + continue + } + diagnostics = append(diagnostics, protocol.Diagnostic{ + Range: entry.refRange, + Severity: protocol.DiagnosticSeverityInformation, + Source: serverName, + Message: fmt.Sprintf( + "%s can be updated (latest: %s)", + identity.IdentityString(), + latestVersion, + ), + }) + } + publishDiagnostics(ctx, m.lsp.client, normalized, diagnostics) + return nil +} + +// parseBufGenYAMLRefs walks the parsed buf.gen.yaml document and collects BSR +// references in document order: plugins[*].remote and inputs[*].module scalar +// values with their source positions. +func parseBufGenYAMLRefs(doc *yaml.Node) ([]bsrRef, []bsrRef, uint32) { + if doc == nil || doc.Kind != yaml.DocumentNode || len(doc.Content) == 0 { + return nil, nil, 0 + } mapping := doc.Content[0] if mapping.Kind != yaml.MappingNode { - return nil + return nil, nil, 0 } - var refs []bsrRef + var refs, versionedPluginRefs []bsrRef + var pluginsKeyLine uint32 for i := 0; i+1 < len(mapping.Content); i += 2 { keyNode := mapping.Content[i] valNode := mapping.Content[i+1] @@ -129,6 +256,7 @@ func parseBufGenYAMLRefs(doc *yaml.Node) []bsrRef { if valNode.Kind != yaml.SequenceNode { continue } + pluginsKeyLine = uint32(keyNode.Line - 1) // yaml.Node.Line is 1-indexed and always ≥ 1 for _, item := range valNode.Content { if item.Kind != yaml.MappingNode { continue @@ -136,7 +264,11 @@ func parseBufGenYAMLRefs(doc *yaml.Node) []bsrRef { for j := 0; j+1 < len(item.Content); j += 2 { k, v := item.Content[j], item.Content[j+1] if k.Value == "remote" && v.Kind == yaml.ScalarNode && v.Value != "" { - refs = append(refs, bsrRef{ref: v.Value, refRange: yamlNodeRange(v)}) + entry := bsrRef{ref: v.Value, refRange: yamlNodeRange(v)} + refs = append(refs, entry) + if _, version, err := bufremotepluginref.ParsePluginIdentityOptionalVersion(v.Value); err == nil && version != "" { + versionedPluginRefs = append(versionedPluginRefs, entry) + } } } } @@ -157,5 +289,5 @@ func parseBufGenYAMLRefs(doc *yaml.Node) []bsrRef { } } } - return refs + return refs, versionedPluginRefs, pluginsKeyLine } diff --git a/private/buf/buflsp/buf_gen_yaml_lsp_test.go b/private/buf/buflsp/buf_gen_yaml_lsp_test.go index 16afa38168..4257faa5b5 100644 --- a/private/buf/buflsp/buf_gen_yaml_lsp_test.go +++ b/private/buf/buflsp/buf_gen_yaml_lsp_test.go @@ -15,14 +15,33 @@ package buflsp_test import ( + "context" "path/filepath" + "slices" "testing" + "testing/synctest" + "time" + "github.com/bufbuild/buf/private/buf/buflsp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.lsp.dev/protocol" ) +// staticCuratedPluginVersionProvider is a test-only curatedPluginVersionProvider +// that returns a fixed latest version for each plugin. +type staticCuratedPluginVersionProvider struct { + latestByPlugin map[string]string +} + +func (p *staticCuratedPluginVersionProvider) GetLatestVersion(_ context.Context, _, owner, plugin string) (string, error) { + v, ok := p.latestByPlugin[owner+"/"+plugin] + if !ok { + return "", nil + } + return v, nil +} + // TestBufGenYAMLDocumentLinks verifies that document links are returned for // remote plugin and input module BSR references in buf.gen.yaml files. func TestBufGenYAMLDocumentLinks(t *testing.T) { @@ -90,7 +109,7 @@ func TestBufGenYAMLDocumentLinks(t *testing.T) { absPath, err := filepath.Abs(tc.fixture) require.NoError(t, err) - clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var links []protocol.DocumentLink @@ -115,7 +134,7 @@ func TestBufGenYAMLHoverMalformedYAML(t *testing.T) { absPath, err := filepath.Abs("testdata/buf_gen_yaml/invalid/buf.gen.yaml") require.NoError(t, err) - clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var hover *protocol.Hover @@ -137,7 +156,7 @@ func TestBufGenYAMLHoverDidChange(t *testing.T) { absPath, err := filepath.Abs("testdata/buf_gen_yaml/hover/buf.gen.yaml") require.NoError(t, err) - clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() // Replace the entire file with minimal content (version key on line 0). @@ -436,7 +455,7 @@ func TestBufGenYAMLHover(t *testing.T) { absPath, err := filepath.Abs(fixture) require.NoError(t, err) - clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() for _, tc := range tests { @@ -465,3 +484,213 @@ func TestBufGenYAMLHover(t *testing.T) { }) } } + +// TestBufGenYAMLCodeLens verifies the code lenses returned for buf.gen.yaml files. +// +// "Run buf generate" is always returned at line 0. +// "Check for plugin updates" is only returned when there are remote plugins with +// explicit version pins, and is positioned at the plugins: key line. +func TestBufGenYAMLCodeLens(t *testing.T) { + t.Parallel() + + // Fixture layout for document_link (0-indexed lines): + // 0: version: v2 + // 1: plugins: + // 2: - remote: buf.build/protocolbuffers/go + // 3: out: gen/go + // 4: - remote: buf.build/bufbuild/es:v2.2.2 ← versioned + // 5: out: gen/es + // 6: - local: protoc-gen-custom + // 7: out: gen/custom + // 8: inputs: + // 9: - module: buf.build/acme/petapis + // 10: - directory: proto + + tests := []struct { + name string + fixture string + wantCount int + wantTitles []string + wantRunLensLine uint32 + wantCheckLensLine uint32 // only checked when "Check for plugin updates" is in wantTitles + }{ + { + name: "no_plugins", + fixture: "testdata/buf_gen_yaml/invalid/buf.gen.yaml", + wantCount: 1, + wantTitles: []string{"Run buf generate"}, + wantRunLensLine: 0, + }, + { + name: "with_versioned_remote_plugin", + fixture: "testdata/buf_gen_yaml/document_link/buf.gen.yaml", + wantCount: 2, + wantTitles: []string{"Run buf generate", "Check for plugin updates"}, + wantRunLensLine: 0, + wantCheckLensLine: 1, // plugins: key is on line 1 + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + absPath, err := filepath.Abs(tc.fixture) + require.NoError(t, err) + + clientJSONConn, bufGenYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) + ctx := t.Context() + + var lenses []protocol.CodeLens + _, err = clientJSONConn.Call(ctx, protocol.MethodTextDocumentCodeLens, &protocol.CodeLensParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: bufGenYAMLURI}, + }, &lenses) + require.NoError(t, err) + require.Len(t, lenses, tc.wantCount) + + for i, l := range lenses { + require.NotNil(t, l.Command, "lens %d has no command", i) + } + titles := make([]string, len(lenses)) + for i, l := range lenses { + titles[i] = l.Command.Title + } + for _, wantTitle := range tc.wantTitles { + assert.Contains(t, titles, wantTitle) + } + + // "Run buf generate" is always at line 0. + for _, l := range lenses { + if l.Command.Title == "Run buf generate" { + assert.Equal(t, tc.wantRunLensLine, l.Range.Start.Line, + "Run buf generate lens should be at line 0") + } + } + + // "Check for plugin updates" is at the plugins: key line when present. + wantCheckLens := slices.Contains(tc.wantTitles, "Check for plugin updates") + if wantCheckLens { + for _, l := range lenses { + if l.Command.Title == "Check for plugin updates" { + assert.Equal(t, tc.wantCheckLensLine, l.Range.Start.Line, + "Check for plugin updates lens should be at the plugins: key line") + } + } + } + }) + } +} + +// TestBufGenYAMLCheckPluginUpdates verifies that the buf.generate.checkPluginUpdates +// command publishes informational diagnostics for outdated remote plugins and none +// for up-to-date ones. +func TestBufGenYAMLCheckPluginUpdates(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + latestVersion string // what the mock BSR returns as the latest version + waitPred func(*protocol.PublishDiagnosticsParams) bool + check func(*testing.T, *protocol.PublishDiagnosticsParams) + }{ + { + name: "up_to_date", + latestVersion: "v2.10.0", // same as pinned in testdata + waitPred: func(_ *protocol.PublishDiagnosticsParams) bool { return true }, + check: func(t *testing.T, diags *protocol.PublishDiagnosticsParams) { + assert.Empty(t, diags.Diagnostics, "expected no diagnostics when plugin is up to date") + }, + }, + { + name: "outdated", + latestVersion: "v2.10.3", // newer than pinned v2.10.0 + waitPred: func(p *protocol.PublishDiagnosticsParams) bool { return len(p.Diagnostics) > 0 }, + check: func(t *testing.T, diags *protocol.PublishDiagnosticsParams) { + require.Len(t, diags.Diagnostics, 1) + d := diags.Diagnostics[0] + assert.Equal(t, protocol.DiagnosticSeverityInformation, d.Severity) + assert.Equal(t, "buf-lsp", d.Source) + assert.Contains(t, d.Message, "buf.build/bufbuild/es") + assert.Contains(t, d.Message, "v2.10.3") + // Diagnostic should be on the remote plugin value line (line 2, 0-indexed). + assert.Equal(t, uint32(2), d.Range.Start.Line) + }, + }, + } + + absPath, err := filepath.Abs("testdata/buf_gen_yaml/with_versioned_plugins/buf.gen.yaml") + require.NoError(t, err) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + cvp := &staticCuratedPluginVersionProvider{ + latestByPlugin: map[string]string{"bufbuild/es": tc.latestVersion}, + } + + synctest.Test(t, func(t *testing.T) { + clientJSONConn, bufGenYAMLURI, capture := setupLSPServerForBufYAML(t, absPath, nil, cvp) + ctx := t.Context() + + var result any + _, err = clientJSONConn.Call(ctx, protocol.MethodWorkspaceExecuteCommand, &protocol.ExecuteCommandParams{ + Command: buflsp.CommandCheckPluginUpdates, + Arguments: []any{string(bufGenYAMLURI)}, + }, &result) + require.NoError(t, err) + + diags := capture.wait(t, bufGenYAMLURI, 5*time.Second, tc.waitPred) + require.NotNil(t, diags) + tc.check(t, diags) + }) + }) + } +} + +// TestBufGenYAMLDiagnostics_ClearedOnClose verifies that diagnostics for a +// buf.gen.yaml file are cleared (empty publish) when the file is closed. +func TestBufGenYAMLDiagnostics_ClearedOnClose(t *testing.T) { + t.Parallel() + + // Make the plugin appear outdated so we get a diagnostic to clear. + cvp := &staticCuratedPluginVersionProvider{ + latestByPlugin: map[string]string{"bufbuild/es": "v2.10.3"}, + } + + absPath, err := filepath.Abs("testdata/buf_gen_yaml/with_versioned_plugins/buf.gen.yaml") + require.NoError(t, err) + + synctest.Test(t, func(t *testing.T) { + clientJSONConn, bufGenYAMLURI, capture := setupLSPServerForBufYAML(t, absPath, nil, cvp) + ctx := t.Context() + + // Trigger a check to produce diagnostics. + var result any + _, err = clientJSONConn.Call(ctx, protocol.MethodWorkspaceExecuteCommand, &protocol.ExecuteCommandParams{ + Command: buflsp.CommandCheckPluginUpdates, + Arguments: []any{string(bufGenYAMLURI)}, + }, &result) + require.NoError(t, err) + + // Wait for the non-empty diagnostic. + diags := capture.wait(t, bufGenYAMLURI, 5*time.Second, func(p *protocol.PublishDiagnosticsParams) bool { + return len(p.Diagnostics) > 0 + }) + require.NotNil(t, diags) + require.NotEmpty(t, diags.Diagnostics, "expected diagnostics before close") + + // Close the file. + err = clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidClose, &protocol.DidCloseTextDocumentParams{ + TextDocument: protocol.TextDocumentIdentifier{URI: bufGenYAMLURI}, + }) + require.NoError(t, err) + + // Diagnostics should now be cleared. + cleared := capture.wait(t, bufGenYAMLURI, 5*time.Second, func(p *protocol.PublishDiagnosticsParams) bool { + return len(p.Diagnostics) == 0 + }) + require.NotNil(t, cleared) + assert.Empty(t, cleared.Diagnostics, "expected diagnostics to be cleared after close") + }) +} diff --git a/private/buf/buflsp/buf_lock_lsp_test.go b/private/buf/buflsp/buf_lock_lsp_test.go index 2cf3391d14..c9acac254f 100644 --- a/private/buf/buflsp/buf_lock_lsp_test.go +++ b/private/buf/buflsp/buf_lock_lsp_test.go @@ -80,7 +80,7 @@ func TestBufLockDocumentLinks(t *testing.T) { absPath, err := filepath.Abs(tc.fixture) require.NoError(t, err) - clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var links []protocol.DocumentLink @@ -105,7 +105,7 @@ func TestBufLockHoverMalformedYAML(t *testing.T) { absPath, err := filepath.Abs("testdata/buf_lock/invalid/buf.lock") require.NoError(t, err) - clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var hover *protocol.Hover @@ -182,7 +182,7 @@ func TestBufLockHover(t *testing.T) { absPath, err := filepath.Abs(fixture) require.NoError(t, err) - clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufLockURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() for _, tc := range tests { diff --git a/private/buf/buflsp/buf_policy_yaml_lsp_test.go b/private/buf/buflsp/buf_policy_yaml_lsp_test.go index 393f4346ad..9b2d012ff1 100644 --- a/private/buf/buflsp/buf_policy_yaml_lsp_test.go +++ b/private/buf/buflsp/buf_policy_yaml_lsp_test.go @@ -88,7 +88,7 @@ func TestBufPolicyYAMLDocumentLinks(t *testing.T) { absPath, err := filepath.Abs(tc.fixture) require.NoError(t, err) - clientJSONConn, bufPolicyYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufPolicyYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var links []protocol.DocumentLink @@ -113,7 +113,7 @@ func TestBufPolicyYAMLHoverMalformedYAML(t *testing.T) { absPath, err := filepath.Abs("testdata/buf_policy_yaml/invalid/buf.policy.yaml") require.NoError(t, err) - clientJSONConn, bufPolicyYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufPolicyYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var hover *protocol.Hover @@ -298,7 +298,7 @@ func TestBufPolicyYAMLHover(t *testing.T) { absPath, err := filepath.Abs(fixture) require.NoError(t, err) - clientJSONConn, bufPolicyYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufPolicyYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() for _, tc := range tests { diff --git a/private/buf/buflsp/buf_yaml.go b/private/buf/buflsp/buf_yaml.go index 4fce63b85d..0ca6037426 100644 --- a/private/buf/buflsp/buf_yaml.go +++ b/private/buf/buflsp/buf_yaml.go @@ -95,7 +95,7 @@ func (m *bufYAMLManager) Close(ctx context.Context, uri protocol.URI) { m.mu.Lock() delete(m.uriToFile, normalized) m.mu.Unlock() - m.publishDiagnostics(ctx, normalized, nil) + publishDiagnostics(ctx, m.lsp.client, normalized, nil) } // GetCodeLenses returns code lenses for the given buf.yaml URI. @@ -166,7 +166,7 @@ func (m *bufYAMLManager) ExecuteCheckUpdates(ctx context.Context, uri protocol.U return fmt.Errorf("getting configured dep module refs: %w", err) } if len(configuredRefs) == 0 { - m.publishDiagnostics(ctx, normalized, nil) + publishDiagnostics(ctx, m.lsp.client, normalized, nil) return nil } @@ -175,9 +175,9 @@ func (m *bufYAMLManager) ExecuteCheckUpdates(ctx context.Context, uri protocol.U if err != nil { return fmt.Errorf("getting existing buf.lock deps: %w", err) } - currentByFullName := make(map[string]bufmodule.ModuleKey, len(currentKeys)) - for _, key := range currentKeys { - currentByFullName[key.FullName().String()] = key + currentByFullName, err := bufparse.FullNameStringToUniqueValue(currentKeys) + if err != nil { + return fmt.Errorf("duplicate module keys in buf.lock: %w", err) } // Build a map from full name → YAML position for each dep entry. @@ -227,16 +227,17 @@ func (m *bufYAMLManager) ExecuteCheckUpdates(ctx context.Context, uri protocol.U ), }) } - m.publishDiagnostics(ctx, normalized, diagnostics) + publishDiagnostics(ctx, m.lsp.client, normalized, diagnostics) return nil } -// publishDiagnostics clears existing diagnostics when passed nil. -func (m *bufYAMLManager) publishDiagnostics(ctx context.Context, uri protocol.URI, diagnostics []protocol.Diagnostic) { +// publishDiagnostics publishes diagnostics to the client, clearing any +// previously published diagnostics for uri when passed nil. +func publishDiagnostics(ctx context.Context, client protocol.Client, uri protocol.URI, diagnostics []protocol.Diagnostic) { if diagnostics == nil { diagnostics = []protocol.Diagnostic{} } - _ = m.lsp.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ + _ = client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ URI: uri, Diagnostics: diagnostics, }) diff --git a/private/buf/buflsp/buf_yaml_lsp_test.go b/private/buf/buflsp/buf_yaml_lsp_test.go index 3699072a69..e9895be284 100644 --- a/private/buf/buflsp/buf_yaml_lsp_test.go +++ b/private/buf/buflsp/buf_yaml_lsp_test.go @@ -15,6 +15,7 @@ package buflsp_test import ( + "context" "net" "net/http" "os" @@ -47,13 +48,23 @@ import ( "go.lsp.dev/uri" ) -// setupLSPServerForBufYAML creates an LSP server initialized for buf.yaml testing. -// It opens the buf.yaml file at bufYAMLPath via didOpen and returns the client -// connection, the buf.yaml URI, and a diagnostics capture for async notifications. +// nopCuratedPluginVersionProvider is a no-op implementation that always reports +// plugins as up to date. +type nopCuratedPluginVersionProvider struct{} + +func (nopCuratedPluginVersionProvider) GetLatestVersion(_ context.Context, _, _, _ string) (string, error) { + return "", nil +} + +// setupLSPServerForBufYAML creates an LSP server initialized for YAML file testing. +// It opens the file at path via didOpen and returns the client connection, the file +// URI, and a diagnostics capture for async notifications. +// Pass nil for mkp or cvp to use the respective no-op provider. func setupLSPServerForBufYAML( t *testing.T, bufYAMLPath string, mkp bufmodule.ModuleKeyProvider, + cvp buflsp.CuratedPluginVersionProvider, ) (jsonrpc2.Conn, protocol.URI, *diagnosticsCapture) { t.Helper() @@ -113,6 +124,10 @@ func setupLSPServerForBufYAML( if mkp != nil { moduleKeyProvider = mkp } + curatedPluginVersionProvider := buflsp.CuratedPluginVersionProvider(nopCuratedPluginVersionProvider{}) + if cvp != nil { + curatedPluginVersionProvider = cvp + } conn, err := buflsp.Serve( ctx, @@ -125,6 +140,7 @@ func setupLSPServerForBufYAML( queryExecutor, moduleKeyProvider, bufmodule.NopGraphProvider, + curatedPluginVersionProvider, ) require.NoError(t, err) t.Cleanup(func() { @@ -210,7 +226,7 @@ func TestBufYAMLCodeLens(t *testing.T) { absPath, err := filepath.Abs(tc.fixture) require.NoError(t, err) - clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var lenses []protocol.CodeLens @@ -332,7 +348,7 @@ func TestBufYAMLCheckUpdates(t *testing.T) { require.NoError(t, err) synctest.Test(t, func(t *testing.T) { - clientJSONConn, bufYAMLURI, capture := setupLSPServerForBufYAML(t, absPath, mkp) + clientJSONConn, bufYAMLURI, capture := setupLSPServerForBufYAML(t, absPath, mkp, nil) ctx := t.Context() var result any @@ -366,7 +382,7 @@ func TestBufYAMLDiagnostics_ClearedOnClose(t *testing.T) { require.NoError(t, err) synctest.Test(t, func(t *testing.T) { - clientJSONConn, bufYAMLURI, capture := setupLSPServerForBufYAML(t, absPath, mkp) + clientJSONConn, bufYAMLURI, capture := setupLSPServerForBufYAML(t, absPath, mkp, nil) ctx := t.Context() // Trigger a check to produce diagnostics. @@ -415,7 +431,7 @@ func TestBufYAMLCheckUpdates_FileChange(t *testing.T) { require.NoError(t, err) synctest.Test(t, func(t *testing.T) { - clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, mkp) + clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, mkp, nil) ctx := t.Context() // Update the buf.yaml in-memory to have no deps. @@ -505,7 +521,7 @@ func TestBufYAMLDocumentLinks(t *testing.T) { absPath, err := filepath.Abs(tc.fixture) require.NoError(t, err) - clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var links []protocol.DocumentLink @@ -809,7 +825,7 @@ func TestBufYAMLHover(t *testing.T) { absPath, err := filepath.Abs(fixture) require.NoError(t, err) - clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() for _, tc := range tests { @@ -872,7 +888,7 @@ func TestBufYAMLHover_OtherFixtures(t *testing.T) { absPath, err := filepath.Abs(tc.fixture) require.NoError(t, err) - clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil) + clientJSONConn, bufYAMLURI, _ := setupLSPServerForBufYAML(t, absPath, nil, nil) ctx := t.Context() var hover *protocol.Hover diff --git a/private/buf/buflsp/buf_yaml_test.go b/private/buf/buflsp/buf_yaml_test.go index 694eeec405..f9961231a7 100644 --- a/private/buf/buflsp/buf_yaml_test.go +++ b/private/buf/buflsp/buf_yaml_test.go @@ -17,6 +17,7 @@ package buflsp import ( "testing" + "github.com/bufbuild/buf/private/bufpkg/bufparse" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.lsp.dev/protocol" @@ -224,3 +225,180 @@ func TestParseYAMLDoc(t *testing.T) { }) } } + +func TestParseBufGenYAMLRefs(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + content string + wantAllRefs []bsrRef + wantVersionedRefs []bsrRef + wantPluginsKeyLine uint32 + }{ + { + name: "nil_doc", + content: "", // parseYAMLDoc returns nil for empty input + }, + { + name: "no_plugins_key", + content: "version: v2\n", + }, + { + name: "local_plugin_only", + content: `version: v2 +plugins: + - local: protoc-gen-go + out: gen/go +`, + wantPluginsKeyLine: 1, + }, + { + name: "unversioned_remote_only", + content: `version: v2 +plugins: + - remote: buf.build/protocolbuffers/go + out: gen/go +`, + wantPluginsKeyLine: 1, + wantAllRefs: []bsrRef{ + { + ref: "buf.build/protocolbuffers/go", + refRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 12}, + End: protocol.Position{Line: 2, Character: 12 + uint32(len("buf.build/protocolbuffers/go"))}, + }, + }, + }, + // unversioned remote is not in versionedPluginRefs + }, + { + name: "versioned_remote", + content: `version: v2 +plugins: + - remote: buf.build/bufbuild/es:v2.2.2 + out: gen/es +`, + wantPluginsKeyLine: 1, + wantAllRefs: []bsrRef{ + { + ref: "buf.build/bufbuild/es:v2.2.2", + refRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 12}, + End: protocol.Position{Line: 2, Character: 12 + uint32(len("buf.build/bufbuild/es:v2.2.2"))}, + }, + }, + }, + // all remote plugins are versioned, so versionedRefs == allRefs + wantVersionedRefs: []bsrRef{ + { + ref: "buf.build/bufbuild/es:v2.2.2", + refRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 12}, + End: protocol.Position{Line: 2, Character: 12 + uint32(len("buf.build/bufbuild/es:v2.2.2"))}, + }, + }, + }, + }, + { + name: "mixed_plugins_and_inputs", + content: `version: v2 +plugins: + - remote: buf.build/bufbuild/es:v2.2.2 + out: gen/es + - remote: buf.build/protocolbuffers/go + out: gen/go + - local: protoc-gen-custom + out: gen/custom +inputs: + - module: buf.build/acme/petapis + - directory: proto +`, + wantPluginsKeyLine: 1, + wantAllRefs: []bsrRef{ + { + ref: "buf.build/bufbuild/es:v2.2.2", + refRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 12}, + End: protocol.Position{Line: 2, Character: 12 + uint32(len("buf.build/bufbuild/es:v2.2.2"))}, + }, + }, + { + ref: "buf.build/protocolbuffers/go", + refRange: protocol.Range{ + Start: protocol.Position{Line: 4, Character: 12}, + End: protocol.Position{Line: 4, Character: 12 + uint32(len("buf.build/protocolbuffers/go"))}, + }, + }, + { + ref: "buf.build/acme/petapis", + refRange: protocol.Range{ + Start: protocol.Position{Line: 9, Character: 12}, + End: protocol.Position{Line: 9, Character: 12 + uint32(len("buf.build/acme/petapis"))}, + }, + }, + }, + wantVersionedRefs: []bsrRef{ + { + ref: "buf.build/bufbuild/es:v2.2.2", + refRange: protocol.Range{ + Start: protocol.Position{Line: 2, Character: 12}, + End: protocol.Position{Line: 2, Character: 12 + uint32(len("buf.build/bufbuild/es:v2.2.2"))}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + doc := parseYAMLDoc(tt.content) + allRefs, versionedRefs, pluginsKeyLine := parseBufGenYAMLRefs(doc) + assert.Equal(t, tt.wantAllRefs, allRefs) + assert.Equal(t, tt.wantVersionedRefs, versionedRefs) + assert.Equal(t, tt.wantPluginsKeyLine, pluginsKeyLine) + }) + } +} + +func TestBsrRefDocURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + refStr string + wantURL string + }{ + { + name: "default_registry_no_ref", + refStr: "buf.build/acme/petapis", + wantURL: "https://buf.build/acme/petapis", + }, + { + name: "default_registry_with_ref", + refStr: "buf.build/bufbuild/es:v2.2.2", + wantURL: "https://buf.build/bufbuild/es/docs/v2.2.2", + }, + { + name: "non_default_registry_with_ref", + // A private BSR host: /docs/ path is not valid, so no suffix. + refStr: "private.example.com/acme/mod:v1.0.0", + wantURL: "https://private.example.com/acme/mod", + }, + { + name: "non_default_registry_no_ref", + refStr: "private.example.com/acme/mod", + wantURL: "https://private.example.com/acme/mod", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ref, err := bufparse.ParseRef(tt.refStr) + require.NoError(t, err) + assert.Equal(t, tt.wantURL, bsrRefDocURL(ref)) + }) + } +} diff --git a/private/buf/buflsp/buflsp.go b/private/buf/buflsp/buflsp.go index 78afd8c968..2f05480017 100644 --- a/private/buf/buflsp/buflsp.go +++ b/private/buf/buflsp/buflsp.go @@ -38,6 +38,13 @@ import ( "go.uber.org/zap" ) +// CuratedPluginVersionProvider checks for the latest version of a BSR-hosted curated plugin. +type CuratedPluginVersionProvider interface { + // GetLatestVersion returns the latest semver version string for the plugin, + // or an empty string if the plugin is unknown or has no published versions. + GetLatestVersion(ctx context.Context, registry, owner, plugin string) (string, error) +} + // Serve spawns a new LSP server, listening on the given stream. // // Returns a context for managing the server. @@ -52,6 +59,7 @@ func Serve( queryExecutor *incremental.Executor, moduleKeyProvider bufmodule.ModuleKeyProvider, graphProvider bufmodule.GraphProvider, + curatedPluginVersionProvider CuratedPluginVersionProvider, ) (jsonrpc2.Conn, error) { logger := container.Logger() logger = logger.With(slog.String("buf_version", bufVersion)) @@ -75,24 +83,25 @@ func Serve( &connWrapper{Conn: conn, logger: logger}, zap.NewNop(), // The logging from protocol itself isn't very good, we've replaced it with connAdapter here. ), - container: container, - logger: logger, - bufVersion: bufVersion, - controller: controller, - wasmRuntime: wasmRuntime, - wktBucket: wktBucket, - queryExecutor: queryExecutor, - opener: source.NewMap(nil), - irSession: new(ir.Session), - connCtx: connCtx, - connCancel: connCancel, - moduleKeyProvider: moduleKeyProvider, - graphProvider: graphProvider, + container: container, + logger: logger, + bufVersion: bufVersion, + controller: controller, + wasmRuntime: wasmRuntime, + wktBucket: wktBucket, + queryExecutor: queryExecutor, + opener: source.NewMap(nil), + irSession: new(ir.Session), + connCtx: connCtx, + connCancel: connCancel, + moduleKeyProvider: moduleKeyProvider, + graphProvider: graphProvider, + curatedPluginVersionProvider: curatedPluginVersionProvider, } lsp.fileManager = newFileManager(lsp) lsp.workspaceManager = newWorkspaceManager(lsp) lsp.bufYAMLManager = newBufYAMLManager(lsp) - lsp.bufGenYAMLManager = newBufGenYAMLManager() + lsp.bufGenYAMLManager = newBufGenYAMLManager(lsp) lsp.bufPolicyYAMLManager = newBufPolicyYAMLManager() lsp.bufLockManager = newBufLockManager() off := protocol.TraceOff @@ -138,10 +147,12 @@ type lsp struct { wktBucket storage.ReadBucket shutdown bool - // moduleKeyProvider resolves module refs to their latest commits (BSR). Set via WithModuleKeyProvider. + // moduleKeyProvider resolves module refs to their latest commits (BSR). moduleKeyProvider bufmodule.ModuleKeyProvider - // graphProvider resolves transitive dependencies for a set of module keys. Set via WithGraphProvider. + // graphProvider resolves transitive dependencies for a set of module keys. graphProvider bufmodule.GraphProvider + // curatedPluginVersionProvider checks for the latest version of curated plugins (BSR). + curatedPluginVersionProvider CuratedPluginVersionProvider lock sync.Mutex diff --git a/private/buf/buflsp/buflsp_test.go b/private/buf/buflsp/buflsp_test.go index f8720f8b04..15d1a04dc4 100644 --- a/private/buf/buflsp/buflsp_test.go +++ b/private/buf/buflsp/buflsp_test.go @@ -132,6 +132,7 @@ func setupLSPServer( queryExecutor, nopModuleKeyProvider{}, bufmodule.NopGraphProvider, + nopCuratedPluginVersionProvider{}, ) require.NoError(t, err) t.Cleanup(func() { diff --git a/private/buf/buflsp/diagnostics_test.go b/private/buf/buflsp/diagnostics_test.go index e50e69d70f..25aa05f85b 100644 --- a/private/buf/buflsp/diagnostics_test.go +++ b/private/buf/buflsp/diagnostics_test.go @@ -127,6 +127,7 @@ func setupLSPServerWithDiagnostics( queryExecutor, nopModuleKeyProvider{}, bufmodule.NopGraphProvider, + nopCuratedPluginVersionProvider{}, ) require.NoError(t, err) t.Cleanup(func() { diff --git a/private/buf/buflsp/document_link.go b/private/buf/buflsp/document_link.go index ef3ec70e0c..63d659534e 100644 --- a/private/buf/buflsp/document_link.go +++ b/private/buf/buflsp/document_link.go @@ -83,11 +83,12 @@ func (s *server) documentLink(file *file) []protocol.DocumentLink { } // bsrRefDocURL builds a BSR page URL from a parsed reference. -// When ref.Ref() is non-empty, the URL points to the versioned /docs/ page. +// When ref.Ref() is non-empty and the registry is the default BSR host, the +// URL points to the versioned /docs/ page. func bsrRefDocURL(ref bufparse.Ref) string { fn := ref.FullName() - url := "https://" + fn.Registry() + "/" + fn.Owner() + "/" + fn.Name() - if ref.Ref() != "" { + url := "https://" + fn.String() + if ref.Ref() != "" && fn.Registry() == bufconnect.DefaultRemote { url += "/docs/" + ref.Ref() } return url @@ -108,15 +109,11 @@ func bsrURL(module bufmodule.Module, pathOrPackage string, anchor string, tabTyp } registry := fullName.Registry() - owner := fullName.Owner() - name := fullName.Name() - - // Default to buf.build if no remote or if it's the default remote if registry == "" { registry = bufconnect.DefaultRemote } - url := "https://" + registry + "/" + owner + "/" + name + "/" + tabType + "/main:" + pathOrPackage + url := "https://" + registry + "/" + fullName.Owner() + "/" + fullName.Name() + "/" + tabType + "/main:" + pathOrPackage if anchor != "" { url += "#" + anchor } diff --git a/private/buf/buflsp/server.go b/private/buf/buflsp/server.go index 0bab237bd0..da5947d16e 100644 --- a/private/buf/buflsp/server.go +++ b/private/buf/buflsp/server.go @@ -168,7 +168,7 @@ func (s *server) Initialize( DocumentLinkProvider: &protocol.DocumentLinkOptions{}, CodeLensProvider: &protocol.CodeLensOptions{}, ExecuteCommandProvider: &protocol.ExecuteCommandOptions{ - Commands: []string{commandUpdateAllDeps, commandCheckUpdates}, + Commands: []string{commandUpdateAllDeps, commandCheckUpdates, CommandRunGenerate, CommandCheckPluginUpdates}, }, }, ServerInfo: info, @@ -383,7 +383,7 @@ func (s *server) DidClose( return nil } if isBufGenYAMLURI(params.TextDocument.URI) { - s.bufGenYAMLManager.Close(params.TextDocument.URI) + s.bufGenYAMLManager.Close(ctx, params.TextDocument.URI) return nil } if isBufPolicyYAMLURI(params.TextDocument.URI) { @@ -624,15 +624,14 @@ func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionPara } // CodeLens is called when the client requests code lenses for a document. -// -// For buf.yaml files, this returns whole-file lenses on the deps: key line to -// trigger dependency updates via the buf.dep.updateAll and buf.dep.checkUpdates -// workspace commands. func (s *server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { - if !isBufYAMLURI(params.TextDocument.URI) { - return nil, nil + if isBufYAMLURI(params.TextDocument.URI) { + return s.bufYAMLManager.GetCodeLenses(params.TextDocument.URI), nil } - return s.bufYAMLManager.GetCodeLenses(params.TextDocument.URI), nil + if isBufGenYAMLURI(params.TextDocument.URI) { + return s.bufGenYAMLManager.GetCodeLenses(params.TextDocument.URI), nil + } + return nil, nil } // ExecuteCommand is called when the client invokes a workspace command registered @@ -642,9 +641,12 @@ func (s *server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) // - buf.dep.updateAll: update all dependencies in the buf.yaml at the given URI. // - buf.dep.checkUpdates: check for newer versions of dependencies and publish // informational diagnostics for any that are outdated. +// - buf.generate.run: run buf generate for the buf.gen.yaml at the given URI. +// - buf.generate.checkPluginUpdates: check for newer versions of remote plugins +// in the buf.gen.yaml and publish informational diagnostics for outdated ones. func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (any, error) { if len(params.Arguments) < 1 { - return nil, fmt.Errorf("%s: expected at least 1 argument (buf.yaml URI), got %d", params.Command, len(params.Arguments)) + return nil, fmt.Errorf("%s: expected at least 1 argument (file URI), got %d", params.Command, len(params.Arguments)) } uriStr, ok := params.Arguments[0].(string) if !ok { @@ -662,6 +664,16 @@ func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCom return nil, fmt.Errorf("%s: %w", params.Command, err) } return nil, nil + case CommandRunGenerate: + if err := s.bufGenYAMLManager.ExecuteRunGenerate(ctx, uri); err != nil { + return nil, fmt.Errorf("%s: %w", params.Command, err) + } + return nil, nil + case CommandCheckPluginUpdates: + if err := s.bufGenYAMLManager.ExecuteCheckPluginUpdates(ctx, uri); err != nil { + return nil, fmt.Errorf("%s: %w", params.Command, err) + } + return nil, nil default: return nil, fmt.Errorf("unknown command: %q", params.Command) } diff --git a/private/buf/buflsp/testdata/buf_gen_yaml/with_versioned_plugins/buf.gen.yaml b/private/buf/buflsp/testdata/buf_gen_yaml/with_versioned_plugins/buf.gen.yaml new file mode 100644 index 0000000000..ea0ef786f3 --- /dev/null +++ b/private/buf/buflsp/testdata/buf_gen_yaml/with_versioned_plugins/buf.gen.yaml @@ -0,0 +1,8 @@ +version: v2 +plugins: + - remote: buf.build/bufbuild/es:v2.10.0 + out: gen/es + - remote: buf.build/protocolbuffers/go + out: gen/go +inputs: + - directory: proto