Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions private/buf/buflsp/buflsp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package buflsp_test

import (
"context"
"net"
"net/http"
"os"
"path/filepath"
"testing"

"buf.build/go/app"
"buf.build/go/app/appext"
"github.com/bufbuild/buf/private/buf/bufctl"
"github.com/bufbuild/buf/private/buf/buflsp"
"github.com/bufbuild/buf/private/buf/bufwkt/bufwktstore"
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
"github.com/bufbuild/buf/private/bufpkg/bufparse"
"github.com/bufbuild/buf/private/bufpkg/bufplugin"
"github.com/bufbuild/buf/private/bufpkg/bufpolicy"
"github.com/bufbuild/buf/private/pkg/git"
"github.com/bufbuild/buf/private/pkg/httpauth"
"github.com/bufbuild/buf/private/pkg/slogtestext"
"github.com/bufbuild/buf/private/pkg/storage/storageos"
"github.com/bufbuild/buf/private/pkg/wasm"
"github.com/bufbuild/protocompile/experimental/incremental"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.lsp.dev/jsonrpc2"
"go.lsp.dev/protocol"
"go.lsp.dev/uri"
)

// nopModuleKeyProvider is a no-op implementation of ModuleKeyProvider for testing
type nopModuleKeyProvider struct{}

func (nopModuleKeyProvider) GetModuleKeysForModuleRefs(context.Context, []bufparse.Ref, bufmodule.DigestType) ([]bufmodule.ModuleKey, error) {
return nil, os.ErrNotExist
}

// setupLSPServer creates and initializes an LSP server for testing.
// Returns the client JSON-RPC connection and the test file URI.
func setupLSPServer(
t *testing.T,
testProtoPath string,
) (jsonrpc2.Conn, protocol.URI) {
t.Helper()

ctx := t.Context()

logger := slogtestext.NewLogger(t)

appContainer, err := app.NewContainerForOS()
require.NoError(t, err)

nameContainer, err := appext.NewNameContainer(appContainer, "buf-test")
require.NoError(t, err)
appextContainer := appext.NewContainer(nameContainer, logger)

graphProvider := bufmodule.NopGraphProvider
moduleDataProvider := bufmodule.NopModuleDataProvider
commitProvider := bufmodule.NopCommitProvider
pluginKeyProvider := bufplugin.NopPluginKeyProvider
pluginDataProvider := bufplugin.NopPluginDataProvider
policyKeyProvider := bufpolicy.NopPolicyKeyProvider
policyDataProvider := bufpolicy.NopPolicyDataProvider

tmpDir := t.TempDir()
storageBucket, err := storageos.NewProvider().NewReadWriteBucket(tmpDir)
require.NoError(t, err)

wktStore := bufwktstore.NewStore(logger, storageBucket)

controller, err := bufctl.NewController(
logger,
appContainer,
graphProvider,
nopModuleKeyProvider{},
moduleDataProvider,
commitProvider,
pluginKeyProvider,
pluginDataProvider,
policyKeyProvider,
policyDataProvider,
wktStore,
&http.Client{},
httpauth.NewNopAuthenticator(),
git.ClonerOptions{},
)
require.NoError(t, err)

wktBucket, err := wktStore.GetBucket(ctx)
require.NoError(t, err)

wasmRuntime, err := wasm.NewRuntime(ctx)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, wasmRuntime.Close(ctx))
})

queryExecutor := incremental.New()

serverConn, clientConn := net.Pipe()
t.Cleanup(func() {
require.NoError(t, serverConn.Close())
require.NoError(t, clientConn.Close())
})

stream := jsonrpc2.NewStream(serverConn)

go func() {
conn, err := buflsp.Serve(
ctx,
wktBucket,
appextContainer,
controller,
wasmRuntime,
stream,
queryExecutor,
)
if err != nil {
t.Errorf("Failed to start server: %v", err)
return
}
t.Cleanup(func() {
require.NoError(t, conn.Close())
})
<-ctx.Done()
}()

clientStream := jsonrpc2.NewStream(clientConn)
clientJSONConn := jsonrpc2.NewConn(clientStream)
clientJSONConn.Go(ctx, jsonrpc2.AsyncHandler(func(_ context.Context, reply jsonrpc2.Replier, _ jsonrpc2.Request) error {
return reply(ctx, nil, nil)
}))
t.Cleanup(func() {
require.NoError(t, clientJSONConn.Close())
})

testWorkspaceDir := filepath.Dir(testProtoPath)
testURI := uri.New(testProtoPath)
var initResult protocol.InitializeResult
_, initErr := clientJSONConn.Call(ctx, protocol.MethodInitialize, &protocol.InitializeParams{
RootURI: uri.New(testWorkspaceDir),
Capabilities: protocol.ClientCapabilities{
TextDocument: &protocol.TextDocumentClientCapabilities{},
},
}, &initResult)
require.NoError(t, initErr)
assert.True(t, initResult.Capabilities.HoverProvider != nil)

err = clientJSONConn.Notify(ctx, protocol.MethodInitialized, &protocol.InitializedParams{})
require.NoError(t, err)

testProtoContent, err := os.ReadFile(testProtoPath)
require.NoError(t, err)

err = clientJSONConn.Notify(ctx, protocol.MethodTextDocumentDidOpen, &protocol.DidOpenTextDocumentParams{
TextDocument: protocol.TextDocumentItem{
URI: testURI,
LanguageID: "protobuf",
Version: 1,
Text: string(testProtoContent),
},
})
require.NoError(t, err)

return clientJSONConn, testURI
}
159 changes: 159 additions & 0 deletions private/buf/buflsp/hover_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package buflsp_test

import (
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.lsp.dev/protocol"
)

func TestHover(t *testing.T) {
t.Parallel()

// if runtime.GOOS == "windows" {
// t.Skip("Skipping on Windows")
// }

ctx := t.Context()

testProtoPath, err := filepath.Abs("testdata/hover/test.proto")
require.NoError(t, err)

clientJSONConn, testURI := setupLSPServer(t, testProtoPath)

tests := []struct {
name string
line uint32
character uint32
expectedContains string
expectNoHover bool
}{
{
name: "hover_on_user_message",
line: 7, // Line with "message User {"
character: 8, // On the word "User"
expectedContains: "User represents a user in the system",
},
{
name: "hover_on_id_field",
line: 9, // Line with "string id = 1;"
character: 10, // On the word "id"
expectedContains: "The unique identifier for the user",
},
{
name: "hover_on_status_enum",
line: 19, // Line with "enum Status {"
character: 5, // On the word "Status"
expectedContains: "Status represents the current state of a user",
},
{
name: "hover_on_status_active",
line: 24, // Line with "STATUS_ACTIVE = 1;"
character: 2, // On "STATUS_ACTIVE"
expectedContains: "The user is active",
},
{
name: "hover_on_user_service",
line: 31, // Line with "service UserService {"
character: 8, // On "UserService"
expectedContains: "UserService provides operations for managing users",
},
{
name: "hover_on_get_user_rpc",
line: 33, // Line with "rpc GetUser"
character: 6, // On "GetUser"
expectedContains: "GetUser retrieves a user by their ID",
},
{
name: "hover_on_status_type_reference",
line: 15, // Line with "Status status = 3;"
character: 2, // On "Status" type
expectedContains: "Status represents the current state of a user",
},
{
name: "hover_on_user_type_reference",
line: 45, // Line with "User user = 1;"
character: 2, // On "User" type
expectedContains: "User represents a user in the system",
},
{
name: "hover_on_rpc_request_type",
line: 33, // Line with "rpc GetUser(GetUserRequest)"
character: 14, // On "GetUserRequest"
expectedContains: "GetUserRequest is the request message for GetUser",
},
{
name: "hover_on_rpc_response_type",
line: 33, // Line with "returns (GetUserResponse)"
character: 39, // On "GetUserResponse"
expectedContains: "GetUserResponse is the response message for GetUser",
},
{
name: "hover_on_syntax_keyword",
line: 0, // Line with "syntax = "proto3";"
character: 0, // On "syntax"
expectNoHover: true,
},
{
name: "hover_on_proto3_string",
line: 0, // Line with "syntax = "proto3";"
character: 10, // On "proto3"
expectNoHover: true,
},
{
name: "hover_on_package_keyword",
line: 2, // Line with "package example.v1;"
character: 0, // On "package"
expectNoHover: true,
},
{
name: "hover_on_package_name",
line: 2, // Line with "package example.v1;"
character: 8, // On "example"
expectNoHover: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var hover *protocol.Hover
_, hoverErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentHover, protocol.HoverParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: testURI,
},
Position: protocol.Position{
Line: tt.line,
Character: tt.character,
},
},
}, &hover)
require.NoError(t, hoverErr)

if tt.expectNoHover {
assert.Nil(t, hover, "expected no hover information")
} else if tt.expectedContains != "" {
require.NotNil(t, hover, "expected hover to be non-nil")
assert.Equal(t, protocol.Markdown, hover.Contents.Kind)
assert.Contains(t, hover.Contents.Value, tt.expectedContains)
}
})
}
}
9 changes: 9 additions & 0 deletions private/buf/buflsp/testdata/hover/buf.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: v2
modules:
- path: .
lint:
use:
- STANDARD
breaking:
use:
- FILE
Loading