-
Notifications
You must be signed in to change notification settings - Fork 277
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: bufdev <[email protected]>
- Loading branch information
Showing
23 changed files
with
3,385 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
// Copyright 2020-2024 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 implements a language server for Protobuf. | ||
// | ||
// The main entry-point of this package is the Serve() function, which creates a new LSP server. | ||
package buflsp | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"sync/atomic" | ||
|
||
"github.com/bufbuild/buf/private/buf/bufctl" | ||
"github.com/bufbuild/buf/private/bufpkg/bufcheck" | ||
"github.com/bufbuild/buf/private/bufpkg/bufimage" | ||
"github.com/bufbuild/buf/private/pkg/app/appext" | ||
"github.com/bufbuild/buf/private/pkg/command" | ||
"github.com/bufbuild/buf/private/pkg/pluginrpcutil" | ||
"github.com/bufbuild/buf/private/pkg/storage" | ||
"github.com/bufbuild/buf/private/pkg/storage/storageos" | ||
"github.com/bufbuild/buf/private/pkg/tracing" | ||
"go.lsp.dev/jsonrpc2" | ||
"go.lsp.dev/protocol" | ||
"go.opentelemetry.io/otel/attribute" | ||
"go.uber.org/zap" | ||
) | ||
|
||
// Serve spawns a new LSP server, listening on the given stream. | ||
// | ||
// Returns a context for managing the server. | ||
func Serve( | ||
ctx context.Context, | ||
container appext.Container, | ||
controller bufctl.Controller, | ||
stream jsonrpc2.Stream, | ||
) (jsonrpc2.Conn, error) { | ||
// The LSP protocol deals with absolute filesystem paths. This requires us to | ||
// bypass the bucket API completely, so we create a bucket pointing at the filesystem | ||
// root. | ||
bucketProvider := storageos.NewProvider(storageos.ProviderWithSymlinks()) | ||
bucket, err := bucketProvider.NewReadWriteBucket( | ||
"/", // TODO: This is not correct for Windows. | ||
storageos.ReadWriteBucketWithSymlinksIfSupported(), | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
tracer := tracing.NewTracer(container.Tracer()) | ||
checkClient, err := bufcheck.NewClient(container.Logger(), tracer, pluginrpcutil.NewRunnerProvider(command.NewRunner()), bufcheck.ClientWithStderr(container.Stderr())) | ||
Check failure on line 62 in private/buf/buflsp/buflsp.go GitHub Actions / test-previous (1.21.x)
Check failure on line 62 in private/buf/buflsp/buflsp.go GitHub Actions / test
Check failure on line 62 in private/buf/buflsp/buflsp.go GitHub Actions / buf-binary-size
Check failure on line 62 in private/buf/buflsp/buflsp.go GitHub Actions / lint
Check failure on line 62 in private/buf/buflsp/buflsp.go GitHub Actions / codeql
Check failure on line 62 in private/buf/buflsp/buflsp.go GitHub Actions / test-previous (1.22.x)
|
||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
conn := jsonrpc2.NewConn(stream) | ||
lsp := &lsp{ | ||
conn: conn, | ||
client: protocol.ClientDispatcher( | ||
&connWrapper{Conn: conn, logger: container.Logger()}, | ||
zap.NewNop(), // The logging from protocol itself isn't very good, we've replaced it with connAdapter here. | ||
), | ||
logger: container.Logger(), | ||
tracer: tracer, | ||
controller: controller, | ||
checkClient: checkClient, | ||
rootBucket: bucket, | ||
} | ||
lsp.fileManager = newFileManager(lsp) | ||
off := protocol.TraceOff | ||
lsp.traceValue.Store(&off) | ||
|
||
conn.Go(ctx, lsp.newHandler()) | ||
return conn, nil | ||
} | ||
|
||
// *** PRIVATE *** | ||
|
||
// lsp contains all of the LSP server's state. (I.e., it is the "god class" the protocol requires | ||
// that we implement). | ||
// | ||
// This type does not implement protocol.Server; see server.go for that. | ||
// This type contains all the necessary book-keeping for keeping the server running. | ||
// Its handler methods are not defined in buflsp.go; they are defined in other files, grouped | ||
// according to the groupings in | ||
type lsp struct { | ||
conn jsonrpc2.Conn | ||
client protocol.Client | ||
|
||
logger *zap.Logger | ||
tracer tracing.Tracer | ||
controller bufctl.Controller | ||
checkClient bufcheck.Client | ||
rootBucket storage.ReadBucket | ||
fileManager *fileManager | ||
|
||
// These are atomics, because they are read often and written to | ||
// almost never, but potentially concurrently. Having them side-by-side | ||
// is fine; they are almost never written to so false sharing is not a | ||
// concern. | ||
initParams atomic.Pointer[protocol.InitializeParams] | ||
traceValue atomic.Pointer[protocol.TraceValue] | ||
} | ||
|
||
// init performs *actual* initialization of the server. This is called by Initialize(). | ||
// | ||
// It may only be called once for a given server. | ||
func (l *lsp) init(params *protocol.InitializeParams) error { | ||
if l.initParams.Load() != nil { | ||
return fmt.Errorf("called the %q method more than once", protocol.MethodInitialize) | ||
} | ||
l.initParams.Store(params) | ||
|
||
// TODO: set up logging. We need to forward everything from server.logger through to | ||
// the client, if tracing is turned on. The right way to do this is with an extra | ||
// goroutine and some channels. | ||
|
||
return nil | ||
} | ||
|
||
// findImportable finds all files that can potentially be imported by the proto file at | ||
// uri. This returns a map from potential Protobuf import path to the URI of the file it would import. | ||
// | ||
// Note that this performs no validation on these files, because those files might be open in the | ||
// editor and might contain invalid syntax at the moment. We only want to get their paths and nothing | ||
// more. | ||
func (l *lsp) findImportable( | ||
ctx context.Context, | ||
uri protocol.URI, | ||
) (map[string]bufimage.ImageFileInfo, error) { | ||
fileInfos, err := l.controller.GetImportableImageFileInfos(ctx, uri.Filename()) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
imports := make(map[string]bufimage.ImageFileInfo) | ||
for _, fileInfo := range fileInfos { | ||
imports[fileInfo.Path()] = fileInfo | ||
} | ||
|
||
l.logger.Sugar().Debugf("found imports for %q: %#v", uri, imports) | ||
|
||
return imports, nil | ||
} | ||
|
||
// newHandler constructs an RPC handler that wraps the default one from jsonrpc2. This allows us | ||
// to inject debug logging, tracing, and timeouts to requests. | ||
func (l *lsp) newHandler() jsonrpc2.Handler { | ||
actual := protocol.ServerHandler(newServer(l), nil) | ||
return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) (retErr error) { | ||
ctx, span := l.tracer.Start( | ||
ctx, | ||
tracing.WithErr(&retErr), | ||
tracing.WithAttributes(attribute.String("method", req.Method())), | ||
) | ||
defer span.End() | ||
|
||
l.logger.Debug( | ||
"processing request", | ||
zap.String("method", req.Method()), | ||
zap.ByteString("params", req.Params()), | ||
) | ||
|
||
ctx = withRequestID(ctx) | ||
|
||
replier := l.wrapReplier(reply, req) | ||
|
||
// Verify that the server has been initialized if this isn't the initialization | ||
// request. | ||
if req.Method() != protocol.MethodInitialize && l.initParams.Load() == nil { | ||
return replier(ctx, nil, fmt.Errorf("the first call to the server must be the %q method", protocol.MethodInitialize)) | ||
} | ||
|
||
return actual(ctx, replier, req) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// Copyright 2020-2024 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. | ||
|
||
// Data for the built-in types. | ||
|
||
package buflsp | ||
|
||
// builtinDocs contains documentation for the built-in types, to display in hover inlays. | ||
var builtinDocs = map[string][]string{ | ||
"int32": { | ||
"A 32-bit integer (varint encoding).", | ||
"", | ||
"Values of this type range between `-2147483648` and `2147483647`.", | ||
"Beware that negative values are encoded as five bytes on the wire!", | ||
}, | ||
"int64": { | ||
"A 64-bit integer (varint encoding).", | ||
"", | ||
"Values of this type range between `-9223372036854775808` and `9223372036854775807`.", | ||
"Beware that negative values are encoded as ten bytes on the wire!", | ||
}, | ||
|
||
"uint32": { | ||
"A 32-bit unsigned integer (varint encoding).", | ||
"", | ||
"Values of this type range between `0` and `4294967295`.", | ||
}, | ||
"uint64": { | ||
"A 64-bit unsigned integer (varint encoding).", | ||
"", | ||
"Values of this type range between `0` and `18446744073709551615`.", | ||
}, | ||
|
||
"sint32": { | ||
"A 32-bit integer (ZigZag encoding).", | ||
"", | ||
"Values of this type range between `-2147483648` and `2147483647`.", | ||
}, | ||
"sint64": { | ||
"A 64-bit integer (ZigZag encoding).", | ||
"", | ||
"Values of this type range between `-9223372036854775808` and `9223372036854775807`.", | ||
}, | ||
|
||
"fixed32": { | ||
"A 32-bit unsigned integer (4-byte encoding).", | ||
"", | ||
"Values of this type range between `0` and `4294967295`.", | ||
}, | ||
"fixed64": { | ||
"A 64-bit unsigned integer (8-byte encoding).", | ||
"", | ||
"Values of this type range between `0` and `18446744073709551615`.", | ||
}, | ||
|
||
"sfixed32": { | ||
"A 32-bit integer (4-byte encoding).", | ||
"", | ||
"Values of this type range between `-2147483648` and `2147483647`.", | ||
}, | ||
"sfixed64": { | ||
"A 64-bit integer (8-byte encoding).", | ||
"", | ||
"Values of this type range between `-9223372036854775808` and `9223372036854775807`.", | ||
}, | ||
|
||
"float": { | ||
"A single-precision floating point number (IEEE-745.2008 binary32).", | ||
}, | ||
"double": { | ||
"A double-precision floating point number (IEEE-745.2008 binary64).", | ||
}, | ||
|
||
"string": { | ||
"A string of text.", | ||
"", | ||
"Stores at most 4GB of text. Intended to be UTF-8 encoded Unicode; use `bytes` if you need other encodings.", | ||
}, | ||
"bytes": { | ||
"A blob of arbitrary bytes.", | ||
"", | ||
"Stores at most 4GB of binary data. Encoded as base64 in JSON.", | ||
}, | ||
|
||
"bool": { | ||
"A Boolean value: `true` or `false`.", | ||
"", | ||
"Encoded as a single byte: `0x00` or `0xff` (all non-zero bytes decode to `true`).", | ||
}, | ||
|
||
"default": { | ||
"A magic option that specifies the field's default value.", | ||
"", | ||
"Unlike every other option on a field, this does not have a corresponding field in", | ||
"`google.protobuf.FieldOptions`; it is implemented by compiler magic.", | ||
}, | ||
} |
Oops, something went wrong.