From 5986f6c97aa9aba8a87a0540be5caca041ab14ea Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Thu, 12 Feb 2026 10:00:12 -0500 Subject: [PATCH 1/3] Add LSP workaround for filepaths with `@` symbols It appears that vscode has it's own URI implementation that will [escape `@` characters][1]. By default, our go.lsp.dev package follows Go's semantics and does not escape these characters. (I plan to report this inconsistency upstream to go.lsp.dev, but don't expect a quick response.) Fixes bufbuild/vscode-buf#569. [1]: https://github.com/microsoft/vscode-uri/blob/65786c7aef8aa1d142fedfde76073cc3549736d2/src/uri.ts#L462 --- private/buf/buflsp/definition_test.go | 46 +++++++++++++++++++ private/buf/buflsp/file_manager.go | 9 ++-- .../buf/buflsp/testdata/uri@encode/buf.yaml | 3 ++ .../buf/buflsp/testdata/uri@encode/test.proto | 45 ++++++++++++++++++ .../buflsp/testdata/uri@encode/types.proto | 11 +++++ private/buf/buflsp/uri.go | 42 +++++++++++++++++ private/buf/buflsp/workspace.go | 3 +- 7 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 private/buf/buflsp/testdata/uri@encode/buf.yaml create mode 100644 private/buf/buflsp/testdata/uri@encode/test.proto create mode 100644 private/buf/buflsp/testdata/uri@encode/types.proto create mode 100644 private/buf/buflsp/uri.go diff --git a/private/buf/buflsp/definition_test.go b/private/buf/buflsp/definition_test.go index f6e8ade769..3b2795210d 100644 --- a/private/buf/buflsp/definition_test.go +++ b/private/buf/buflsp/definition_test.go @@ -16,6 +16,7 @@ package buflsp_test import ( "path/filepath" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -206,3 +207,48 @@ func TestDefinition(t *testing.T) { }) } } + +// TestDefinitionURLEncoding verifies that file paths with special characters +// like '@' are properly URL-encoded in the URI responses. +func TestDefinitionURLEncoding(t *testing.T) { + t.Parallel() + + ctx := t.Context() + + // Use a file from a directory with '@' in the path + testProtoPath, err := filepath.Abs("testdata/uri@encode/test.proto") + require.NoError(t, err) + + clientJSONConn, testURI := setupLSPServer(t, testProtoPath) + + // Note: The client may send URIs with unencoded @ symbols, but the LSP + // server normalizes them internally to ensure consistency + + // Test definition lookup for a type reference within the same file + var locations []protocol.Location + _, defErr := clientJSONConn.Call(ctx, protocol.MethodTextDocumentDefinition, protocol.DefinitionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: testURI, + }, + Position: protocol.Position{ + Line: 13, // Line with "Status status = 3;" (0-indexed, line 14 in file) + Character: 2, // On "Status" type + }, + }, + }, &locations) + require.NoError(t, defErr) + + require.Len(t, locations, 1, "expected exactly one definition location") + location := locations[0] + + // Construct the expected URI with @ encoded as %40 + expectedURIPath := filepath.ToSlash(testProtoPath) + expectedURIPath = strings.ReplaceAll(expectedURIPath, "@", "%40") + expectedURI := protocol.URI("file://" + expectedURIPath) + + assert.Equal(t, expectedURI, location.URI, "returned URI should have @ encoded as %40") + + // Verify it points to the correct location in the file + assert.Equal(t, uint32(17), location.Range.Start.Line, "should point to Status enum definition (0-indexed, line 18 in file)") +} diff --git a/private/buf/buflsp/file_manager.go b/private/buf/buflsp/file_manager.go index 8d8436caa5..89822ebe1b 100644 --- a/private/buf/buflsp/file_manager.go +++ b/private/buf/buflsp/file_manager.go @@ -40,17 +40,18 @@ func newFileManager(lsp *lsp) *fileManager { // // This will increment the file's refcount. func (fm *fileManager) Track(uri protocol.URI) *file { - file, found := fm.uriToFile.Insert(uri) + normalizedURI := normalizeURI(uri) + file, found := fm.uriToFile.Insert(normalizedURI) if !found { file.lsp = fm.lsp - file.uri = uri + file.uri = normalizedURI } return file } // Get finds a file with the given URI, or returns nil. func (fm *fileManager) Get(uri protocol.URI) *file { - return fm.uriToFile.Get(uri) + return fm.uriToFile.Get(normalizeURI(uri)) } // Close marks a file as closed. @@ -58,7 +59,7 @@ func (fm *fileManager) Get(uri protocol.URI) *file { // This will not necessarily evict the file, since there may be more than one user // for this file. func (fm *fileManager) Close(ctx context.Context, uri protocol.URI) { - if deleted := fm.uriToFile.Delete(uri); deleted != nil { + if deleted := fm.uriToFile.Delete(normalizeURI(uri)); deleted != nil { deleted.Reset(ctx) } } diff --git a/private/buf/buflsp/testdata/uri@encode/buf.yaml b/private/buf/buflsp/testdata/uri@encode/buf.yaml new file mode 100644 index 0000000000..b8699818a0 --- /dev/null +++ b/private/buf/buflsp/testdata/uri@encode/buf.yaml @@ -0,0 +1,3 @@ +version: v2 +modules: + - path: . diff --git a/private/buf/buflsp/testdata/uri@encode/test.proto b/private/buf/buflsp/testdata/uri@encode/test.proto new file mode 100644 index 0000000000..9d86c0bd71 --- /dev/null +++ b/private/buf/buflsp/testdata/uri@encode/test.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package uri.encode.v1; + +// User represents a user in the system. +message User { + // The unique identifier for the user. + string id = 1; + + // The user's email address. + string email = 2; + + // The user's current status. + Status status = 3; +} + +// Status represents the current state of a user. +enum Status { + // The status is not specified. + STATUS_UNSPECIFIED = 0; + + // The user is active. + STATUS_ACTIVE = 1; + + // The user is inactive. + STATUS_INACTIVE = 2; +} + +// UserService provides operations for managing users. +service UserService { + // GetUser retrieves a user by their ID. + rpc GetUser(GetUserRequest) returns (GetUserResponse); +} + +// GetUserRequest is the request message for GetUser. +message GetUserRequest { + // The ID of the user to retrieve. + string user_id = 1; +} + +// GetUserResponse is the response message for GetUser. +message GetUserResponse { + // The retrieved user. + User user = 1; +} diff --git a/private/buf/buflsp/testdata/uri@encode/types.proto b/private/buf/buflsp/testdata/uri@encode/types.proto new file mode 100644 index 0000000000..a10569eea3 --- /dev/null +++ b/private/buf/buflsp/testdata/uri@encode/types.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package uri.encode.v1; + +// Address represents a physical address. +message Address { + string street = 1; + string city = 2; + string state = 3; + string zip_code = 4; +} diff --git a/private/buf/buflsp/uri.go b/private/buf/buflsp/uri.go new file mode 100644 index 0000000000..d1ff32ad6f --- /dev/null +++ b/private/buf/buflsp/uri.go @@ -0,0 +1,42 @@ +// 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 + +import ( + "strings" + + "go.lsp.dev/protocol" + "go.lsp.dev/uri" +) + +// normalizeURI ensures that URIs are properly percent-encoded for LSP compatibility. +// +// The go.lsp.dev/uri package (which uses Go's net/url) follows RFC 3986 strictly and +// allows '@' unencoded in path components. However, VS Code's LSP client uses the +// microsoft/vscode-uri package which encodes '@' as '%40' everywhere to avoid ambiguity +// with the authority component separator (user@host). +// +// When URIs don't match exactly, LSP operations like go-to-definition fail because +// the client's URI (with %40) doesn't match the server's URI (with @). +func normalizeURI(u protocol.URI) protocol.URI { + s := string(u) + s = strings.ReplaceAll(s, "@", "%40") + return protocol.URI(s) +} + +// filePathToURI converts a file path to a properly encoded URI. +func filePathToURI(path string) protocol.URI { + return normalizeURI(protocol.URI(uri.File(path))) +} diff --git a/private/buf/buflsp/workspace.go b/private/buf/buflsp/workspace.go index 06ec81e339..7081c2cf5d 100644 --- a/private/buf/buflsp/workspace.go +++ b/private/buf/buflsp/workspace.go @@ -29,7 +29,6 @@ import ( "github.com/bufbuild/buf/private/pkg/normalpath" "github.com/bufbuild/buf/private/pkg/storage" "go.lsp.dev/protocol" - "go.lsp.dev/uri" ) // errUnresolvableWorkspace is an unsupported workspace error. @@ -248,7 +247,7 @@ func (w *workspace) indexFiles(ctx context.Context) { for fileInfo := range w.fileInfos(ctx) { file, ok := previous[fileInfo.Path()] if !ok { - fileURI := uri.File(fileInfo.LocalPath()) + fileURI := filePathToURI(fileInfo.LocalPath()) file = w.lsp.fileManager.Track(fileURI) w.lsp.logger.Debug("workspace: index track file", slog.String("path", file.uri.Filename())) } From 34f4d389ff87f6244989707157447b119aa20107 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Thu, 12 Feb 2026 10:09:21 -0500 Subject: [PATCH 2/3] Fix lint, inline normalizeURI --- private/buf/buflsp/uri.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/private/buf/buflsp/uri.go b/private/buf/buflsp/uri.go index d1ff32ad6f..c047d9c932 100644 --- a/private/buf/buflsp/uri.go +++ b/private/buf/buflsp/uri.go @@ -31,12 +31,10 @@ import ( // When URIs don't match exactly, LSP operations like go-to-definition fail because // the client's URI (with %40) doesn't match the server's URI (with @). func normalizeURI(u protocol.URI) protocol.URI { - s := string(u) - s = strings.ReplaceAll(s, "@", "%40") - return protocol.URI(s) + return protocol.URI(strings.ReplaceAll(string(u), "@", "%40")) } // filePathToURI converts a file path to a properly encoded URI. func filePathToURI(path string) protocol.URI { - return normalizeURI(protocol.URI(uri.File(path))) + return normalizeURI(uri.File(path)) } From 665c7249989eafc811b6a1e88c5b87b45504a004 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Thu, 12 Feb 2026 10:21:25 -0500 Subject: [PATCH 3/3] Try to fix for windows --- private/buf/buflsp/definition_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/private/buf/buflsp/definition_test.go b/private/buf/buflsp/definition_test.go index 3b2795210d..20f79d4040 100644 --- a/private/buf/buflsp/definition_test.go +++ b/private/buf/buflsp/definition_test.go @@ -243,9 +243,8 @@ func TestDefinitionURLEncoding(t *testing.T) { location := locations[0] // Construct the expected URI with @ encoded as %40 - expectedURIPath := filepath.ToSlash(testProtoPath) - expectedURIPath = strings.ReplaceAll(expectedURIPath, "@", "%40") - expectedURI := protocol.URI("file://" + expectedURIPath) + // Use uri.File() to get the correct URI format for the platform (e.g., file:/// on Windows) + expectedURI := protocol.URI(strings.ReplaceAll(string(uri.File(testProtoPath)), "@", "%40")) assert.Equal(t, expectedURI, location.URI, "returned URI should have @ encoded as %40")