diff --git a/private/buf/buflsp/document_symbol_test.go b/private/buf/buflsp/document_symbol_test.go new file mode 100644 index 0000000000..dbff532625 --- /dev/null +++ b/private/buf/buflsp/document_symbol_test.go @@ -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) + } + }) + } +} diff --git a/private/buf/buflsp/references_test.go b/private/buf/buflsp/references_test.go new file mode 100644 index 0000000000..2d83787518 --- /dev/null +++ b/private/buf/buflsp/references_test.go @@ -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) + } + }) + } +} diff --git a/private/buf/buflsp/testdata/references/buf.yaml b/private/buf/buflsp/testdata/references/buf.yaml new file mode 100644 index 0000000000..f74da98a3c --- /dev/null +++ b/private/buf/buflsp/testdata/references/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/private/buf/buflsp/testdata/references/references.proto b/private/buf/buflsp/testdata/references/references.proto new file mode 100644 index 0000000000..afc9c214c8 --- /dev/null +++ b/private/buf/buflsp/testdata/references/references.proto @@ -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; +} diff --git a/private/buf/buflsp/testdata/references/types.proto b/private/buf/buflsp/testdata/references/types.proto new file mode 100644 index 0000000000..eddfbd9faf --- /dev/null +++ b/private/buf/buflsp/testdata/references/types.proto @@ -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; +} diff --git a/private/buf/buflsp/testdata/symbols/buf.yaml b/private/buf/buflsp/testdata/symbols/buf.yaml new file mode 100644 index 0000000000..f74da98a3c --- /dev/null +++ b/private/buf/buflsp/testdata/symbols/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/private/buf/buflsp/testdata/symbols/symbols.proto b/private/buf/buflsp/testdata/symbols/symbols.proto new file mode 100644 index 0000000000..1af9a100ea --- /dev/null +++ b/private/buf/buflsp/testdata/symbols/symbols.proto @@ -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; +} diff --git a/private/buf/buflsp/testdata/workspace_symbols/buf.yaml b/private/buf/buflsp/testdata/workspace_symbols/buf.yaml new file mode 100644 index 0000000000..f74da98a3c --- /dev/null +++ b/private/buf/buflsp/testdata/workspace_symbols/buf.yaml @@ -0,0 +1,9 @@ +version: v2 +modules: + - path: . +lint: + use: + - STANDARD +breaking: + use: + - FILE diff --git a/private/buf/buflsp/testdata/workspace_symbols/types.proto b/private/buf/buflsp/testdata/workspace_symbols/types.proto new file mode 100644 index 0000000000..a92306ec3a --- /dev/null +++ b/private/buf/buflsp/testdata/workspace_symbols/types.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package workspace_symbols.v1; + +enum Color { + COLOR_UNSPECIFIED = 0; + COLOR_RED = 1; + COLOR_BLUE = 2; +} + +message Label { + string key = 1; + string value = 2; +} diff --git a/private/buf/buflsp/testdata/workspace_symbols/workspace_symbols.proto b/private/buf/buflsp/testdata/workspace_symbols/workspace_symbols.proto new file mode 100644 index 0000000000..a307cbab23 --- /dev/null +++ b/private/buf/buflsp/testdata/workspace_symbols/workspace_symbols.proto @@ -0,0 +1,44 @@ +syntax = "proto3"; + +package workspace_symbols.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; +} + +message LegacyItem { + option deprecated = true; + string id = 1; +} diff --git a/private/buf/buflsp/workspace_symbol_test.go b/private/buf/buflsp/workspace_symbol_test.go new file mode 100644 index 0000000000..6799fe2b97 --- /dev/null +++ b/private/buf/buflsp/workspace_symbol_test.go @@ -0,0 +1,136 @@ +// 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 TestWorkspaceSymbol(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + testProtoPath, err := filepath.Abs("testdata/workspace_symbols/workspace_symbols.proto") + require.NoError(t, err) + + typesProtoPath, err := filepath.Abs("testdata/workspace_symbols/types.proto") + require.NoError(t, err) + + clientJSONConn, testURI := setupLSPServer(t, testProtoPath) + typesURI := uri.New(typesProtoPath) + + type symbolInfo struct { + name string + kind protocol.SymbolKind + line uint32 + deprecated bool + uri protocol.URI + } + + tests := []struct { + name string + query string + expectedSymbols []symbolInfo // Symbols that should be found with their details + minResults int // Minimum number of results expected + }{ + { + name: "search_for_item", + query: "Item", + expectedSymbols: []symbolInfo{ + {name: "workspace_symbols.v1.Item", kind: protocol.SymbolKindClass, line: 6, uri: testURI}, + {name: "workspace_symbols.v1.GetItemRequest", kind: protocol.SymbolKindClass, line: 24, uri: testURI}, + {name: "workspace_symbols.v1.GetItemResponse", kind: protocol.SymbolKindClass, line: 28, uri: testURI}, + {name: "workspace_symbols.v1.ListItemsRequest", kind: protocol.SymbolKindClass, line: 32, uri: testURI}, + {name: "workspace_symbols.v1.ListItemsResponse", kind: protocol.SymbolKindClass, line: 36, uri: testURI}, + {name: "workspace_symbols.v1.ItemService", kind: protocol.SymbolKindInterface, line: 19, uri: testURI}, + }, + minResults: 6, + }, + { + name: "search_for_color", + query: "Color", + expectedSymbols: []symbolInfo{ + {name: "workspace_symbols.v1.Color", kind: protocol.SymbolKindEnum, line: 4, uri: typesURI}, + {name: "workspace_symbols.v1.COLOR_UNSPECIFIED", kind: protocol.SymbolKindEnumMember, line: 5, uri: typesURI}, + {name: "workspace_symbols.v1.COLOR_RED", kind: protocol.SymbolKindEnumMember, line: 6, uri: typesURI}, + {name: "workspace_symbols.v1.COLOR_BLUE", kind: protocol.SymbolKindEnumMember, line: 7, uri: typesURI}, + }, + minResults: 4, + }, + { + name: "search_for_label", + query: "Label", + expectedSymbols: []symbolInfo{ + {name: "workspace_symbols.v1.Label", kind: protocol.SymbolKindClass, line: 10, uri: typesURI}, + }, + minResults: 1, + }, + { + name: "search_for_container", + query: "Container", + expectedSymbols: []symbolInfo{ + {name: "workspace_symbols.v1.Container", kind: protocol.SymbolKindClass, line: 13, uri: testURI}, + }, + minResults: 1, + }, + { + name: "search_for_deprecated", + query: "Legacy", + expectedSymbols: []symbolInfo{ + {name: "workspace_symbols.v1.LegacyItem", kind: protocol.SymbolKindClass, line: 40, deprecated: true, uri: testURI}, + }, + minResults: 1, + }, + { + name: "empty_query_returns_all_symbols", + query: "", + minResults: 20, // Should return many symbols from both files + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var symbols []protocol.SymbolInformation + _, symErr := clientJSONConn.Call(ctx, protocol.MethodWorkspaceSymbol, protocol.WorkspaceSymbolParams{ + Query: tt.query, + }, &symbols) + require.NoError(t, symErr) + + assert.GreaterOrEqual(t, len(symbols), tt.minResults) + + 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, expectedSymbol.uri, 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) + } + }) + } +}