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
106 changes: 106 additions & 0 deletions private/buf/buflsp/document_symbol_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// 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"
"slices"
"testing"

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

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

ctx := t.Context()

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

clientJSONConn, testURI := setupLSPServer(t, testProtoPath)

type symbolInfo struct {
name string
kind protocol.SymbolKind
line uint32
deprecated bool
}
tests := []struct {
name string
expectedSymbols []symbolInfo
}{
{
name: "all_document_symbols",
expectedSymbols: []symbolInfo{
{name: "symbols.v1.Document", kind: protocol.SymbolKindClass, line: 4}, // message Document
{name: "symbols.v1.Document.id", kind: protocol.SymbolKindField, line: 5}, // string id
{name: "symbols.v1.Document.title", kind: protocol.SymbolKindField, line: 6}, // string title
{name: "symbols.v1.Document.status", kind: protocol.SymbolKindField, line: 7}, // Status status
{name: "symbols.v1.Document.metadata", kind: protocol.SymbolKindField, line: 8}, // Metadata metadata
{name: "symbols.v1.Document.Metadata", kind: protocol.SymbolKindClass, line: 9}, // message Metadata (nested)
{name: "symbols.v1.Document.Metadata.author", kind: protocol.SymbolKindField, line: 10}, // string author
{name: "symbols.v1.Document.Metadata.created_at", kind: protocol.SymbolKindField, line: 11}, // int64 created_at
{name: "symbols.v1.Status", kind: protocol.SymbolKindEnum, line: 15}, // enum Status
{name: "symbols.v1.STATUS_UNSPECIFIED", kind: protocol.SymbolKindEnumMember, line: 16}, // STATUS_UNSPECIFIED = 0
{name: "symbols.v1.STATUS_DRAFT", kind: protocol.SymbolKindEnumMember, line: 17}, // STATUS_DRAFT = 1
{name: "symbols.v1.STATUS_PUBLISHED", kind: protocol.SymbolKindEnumMember, line: 18}, // STATUS_PUBLISHED = 2
{name: "symbols.v1.DocumentService", kind: protocol.SymbolKindInterface, line: 21}, // service DocumentService
{name: "symbols.v1.DocumentService.GetDocument", kind: protocol.SymbolKindMethod, line: 22}, // rpc GetDocument
{name: "symbols.v1.DocumentService.CreateDocument", kind: protocol.SymbolKindMethod, line: 23}, // rpc CreateDocument
{name: "symbols.v1.GetDocumentRequest", kind: protocol.SymbolKindClass, line: 26}, // message GetDocumentRequest
{name: "symbols.v1.GetDocumentRequest.document_id", kind: protocol.SymbolKindField, line: 27}, // string document_id
{name: "symbols.v1.GetDocumentResponse", kind: protocol.SymbolKindClass, line: 30}, // message GetDocumentResponse
{name: "symbols.v1.GetDocumentResponse.document", kind: protocol.SymbolKindField, line: 31}, // Document document
{name: "symbols.v1.CreateDocumentRequest", kind: protocol.SymbolKindClass, line: 34}, // message CreateDocumentRequest
{name: "symbols.v1.CreateDocumentRequest.document", kind: protocol.SymbolKindField, line: 35}, // Document document
{name: "symbols.v1.CreateDocumentResponse", kind: protocol.SymbolKindClass, line: 38}, // message CreateDocumentResponse
{name: "symbols.v1.CreateDocumentResponse.document", kind: protocol.SymbolKindField, line: 39}, // Document document
{name: "symbols.v1.LegacyDocument", kind: protocol.SymbolKindClass, line: 42, deprecated: true}, // message LegacyDocument (deprecated)
{name: "symbols.v1.LegacyDocument.id", kind: protocol.SymbolKindField, line: 44}, // string id
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var symbols []protocol.SymbolInformation
_, symErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentDocumentSymbol, protocol.DocumentSymbolParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: testURI,
},
}, &symbols)
require.NoError(t, symErr)

require.Len(t, symbols, len(tt.expectedSymbols))

for _, expectedSymbol := range tt.expectedSymbols {
idx := slices.IndexFunc(symbols, func(s protocol.SymbolInformation) bool {
return s.Name == expectedSymbol.name
})
require.NotEqual(t, -1, idx, "expected to find symbol %s", expectedSymbol.name)
found := symbols[idx]
assert.Equal(t, expectedSymbol.kind, found.Kind, "symbol %s has wrong kind", expectedSymbol.name)
assert.Equal(t, testURI, found.Location.URI, "symbol %s has wrong URI", expectedSymbol.name)
assert.Equal(t, expectedSymbol.line, found.Location.Range.Start.Line, "symbol %s has wrong line number", expectedSymbol.name)
assert.Equal(t, expectedSymbol.deprecated, found.Deprecated, "symbol %s has wrong deprecated status", expectedSymbol.name)
}
})
}
}
168 changes: 168 additions & 0 deletions private/buf/buflsp/references_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// 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"
"slices"
"testing"

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

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

ctx := t.Context()

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

typesProtoPath, err := filepath.Abs("testdata/references/types.proto")
require.NoError(t, err)

clientJSONConn, testURI := setupLSPServer(t, testProtoPath)
typesURI := uri.New(typesProtoPath)

type refLocation struct {
uri protocol.URI
line uint32
}
tests := []struct {
name string
targetURI protocol.URI
line uint32
character uint32
includeDeclaration bool
expectedReferences []refLocation
}{
{
name: "references_to_item_message",
targetURI: testURI,
line: 6,
character: 8,
includeDeclaration: true,
expectedReferences: []refLocation{
{uri: testURI, line: 6}, // message Item
{uri: testURI, line: 10}, // repeated Item related
{uri: testURI, line: 15}, // repeated Item items in Container
{uri: testURI, line: 29}, // Item item in GetItemResponse
{uri: testURI, line: 37}, // repeated Item items in ListItemsResponse
},
},
{
name: "references_to_item_message_no_declaration",
targetURI: testURI,
line: 6,
character: 8,
includeDeclaration: false,
expectedReferences: []refLocation{
{uri: testURI, line: 10}, // repeated Item related
{uri: testURI, line: 15}, // repeated Item items in Container
{uri: testURI, line: 29}, // Item item in GetItemResponse
{uri: testURI, line: 37}, // repeated Item items in ListItemsResponse
},
},
{
name: "references_to_color_enum_imported",
targetURI: typesURI,
line: 4,
character: 5,
includeDeclaration: true,
expectedReferences: []refLocation{
{uri: typesURI, line: 4}, // enum Color
{uri: testURI, line: 8}, // Color color in Item
{uri: testURI, line: 16}, // Color default_color in Container
},
},
{
name: "references_to_container_message",
targetURI: testURI,
line: 13,
character: 8,
includeDeclaration: true,
expectedReferences: []refLocation{
{uri: testURI, line: 13}, // message Container
},
},
{
name: "references_to_label_imported_type",
targetURI: typesURI,
line: 10,
character: 8,
includeDeclaration: true,
expectedReferences: []refLocation{
{uri: typesURI, line: 10}, // message Label
{uri: testURI, line: 9}, // Label label in Item
},
},
{
name: "references_to_get_item_request",
targetURI: testURI,
line: 24,
character: 8,
includeDeclaration: true,
expectedReferences: []refLocation{
{uri: testURI, line: 24}, // message GetItemRequest
{uri: testURI, line: 20}, // rpc GetItem(GetItemRequest)
},
},
{
name: "references_to_service",
targetURI: testURI,
line: 19,
character: 8,
includeDeclaration: true,
expectedReferences: []refLocation{
{uri: testURI, line: 19}, // service ItemService
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var locations []protocol.Location
_, refErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentReferences, protocol.ReferenceParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: protocol.TextDocumentIdentifier{
URI: tt.targetURI,
},
Position: protocol.Position{
Line: tt.line,
Character: tt.character,
},
},
Context: protocol.ReferenceContext{
IncludeDeclaration: tt.includeDeclaration,
},
}, &locations)
require.NoError(t, refErr)

require.Len(t, locations, len(tt.expectedReferences))

for _, expectedRef := range tt.expectedReferences {
idx := slices.IndexFunc(locations, func(loc protocol.Location) bool {
return loc.URI == expectedRef.uri && loc.Range.Start.Line == expectedRef.line
})
assert.NotEqual(t, -1, idx, "expected reference at %s:%d not found", expectedRef.uri, expectedRef.line)
}
})
}
}
9 changes: 9 additions & 0 deletions private/buf/buflsp/testdata/references/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
39 changes: 39 additions & 0 deletions private/buf/buflsp/testdata/references/references.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
syntax = "proto3";

package references.v1;

import "types.proto";

message Item {
string id = 1;
Color color = 2;
Label label = 3;
repeated Item related = 4;
}

message Container {
string name = 1;
repeated Item items = 2;
Color default_color = 3;
}

service ItemService {
rpc GetItem(GetItemRequest) returns (GetItemResponse);
rpc ListItems(ListItemsRequest) returns (ListItemsResponse);
}

message GetItemRequest {
string item_id = 1;
}

message GetItemResponse {
Item item = 1;
}

message ListItemsRequest {
string container_name = 1;
}

message ListItemsResponse {
repeated Item items = 1;
}
14 changes: 14 additions & 0 deletions private/buf/buflsp/testdata/references/types.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax = "proto3";

package references.v1;

enum Color {
COLOR_UNSPECIFIED = 0;
COLOR_RED = 1;
COLOR_BLUE = 2;
}

message Label {
string key = 1;
string value = 2;
}
9 changes: 9 additions & 0 deletions private/buf/buflsp/testdata/symbols/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
46 changes: 46 additions & 0 deletions private/buf/buflsp/testdata/symbols/symbols.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
syntax = "proto3";

package symbols.v1;

message Document {
string id = 1;
string title = 2;
Status status = 3;
Metadata metadata = 4;
message Metadata {
string author = 1;
int64 created_at = 2;
}
}

enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_DRAFT = 1;
STATUS_PUBLISHED = 2;
}

service DocumentService {
rpc GetDocument(GetDocumentRequest) returns (GetDocumentResponse);
rpc CreateDocument(CreateDocumentRequest) returns (CreateDocumentResponse);
}

message GetDocumentRequest {
string document_id = 1;
}

message GetDocumentResponse {
Document document = 1;
}

message CreateDocumentRequest {
Document document = 1;
}

message CreateDocumentResponse {
Document document = 1;
}

message LegacyDocument {
option deprecated = true;
string id = 1;
}
Loading