Skip to content

Commit 3c62363

Browse files
authored
LSP fix positional encoding for diagnostics (#4066)
1 parent 18712c7 commit 3c62363

File tree

4 files changed

+70
-26
lines changed

4 files changed

+70
-26
lines changed

private/buf/buflsp/file.go

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/bufbuild/protocompile/experimental/incremental"
4545
"github.com/bufbuild/protocompile/experimental/incremental/queries"
4646
"github.com/bufbuild/protocompile/experimental/ir"
47+
"github.com/bufbuild/protocompile/experimental/report"
4748
"github.com/bufbuild/protocompile/experimental/seq"
4849
"github.com/bufbuild/protocompile/experimental/source"
4950
"go.lsp.dev/protocol"
@@ -63,7 +64,7 @@ type file struct {
6364
uri protocol.URI
6465
checkWork chan<- struct{}
6566

66-
text string
67+
file *report.File
6768
// Version is an opaque version identifier given to us by the LSP client. This
6869
// is used in the protocol to disambiguate which version of a file e.g. publishing
6970
// diagnostics or symbols an operating refers to.
@@ -153,13 +154,19 @@ func (f *file) ReadFromDisk(ctx context.Context) (err error) {
153154
return nil
154155
}
155156

156-
data, err := os.ReadFile(f.uri.Filename())
157+
reader, err := os.Open(f.uri.Filename())
158+
if err != nil {
159+
return fmt.Errorf("could not open file %q from disk: %w", f.uri, err)
160+
}
161+
defer reader.Close()
162+
text, err := readAllAsString(reader)
157163
if err != nil {
158164
return fmt.Errorf("could not read file %q from disk: %w", f.uri, err)
159165
}
160166

161167
f.version = -1
162-
f.text = string(data)
168+
f.file = report.NewFile(f.objectInfo.Path(), text)
169+
f.hasText = true
163170
return nil
164171
}
165172

@@ -170,7 +177,7 @@ func (f *file) Update(ctx context.Context, version int32, text string) {
170177

171178
f.lsp.logger.Info(fmt.Sprintf("new file version: %v, %v -> %v", f.uri, f.version, version))
172179
f.version = version
173-
f.text = text
180+
f.file = report.NewFile(f.objectInfo.Path(), text)
174181
f.hasText = true
175182
}
176183

@@ -435,11 +442,11 @@ func (f *file) RefreshIR(ctx context.Context) {
435442
)
436443

437444
openerMap := map[string]string{
438-
f.objectInfo.Path(): f.text,
445+
f.objectInfo.Path(): f.file.Text(),
439446
}
440447
files := []*file{f}
441448
for path, file := range f.importToFile {
442-
openerMap[path] = file.text
449+
openerMap[path] = file.file.Text()
443450
files = append(files, file)
444451
}
445452
opener := source.NewMap(openerMap)
@@ -939,7 +946,7 @@ func (f *file) newFileOpener() fileOpener {
939946
if file == nil {
940947
return nil, fmt.Errorf("%s: %w", path, fs.ErrNotExist)
941948
}
942-
return xio.CompositeReadCloser(strings.NewReader(file.text), xio.NopCloser), nil
949+
return xio.CompositeReadCloser(strings.NewReader(file.file.Text()), xio.NopCloser), nil
943950
}
944951
}
945952

@@ -1095,24 +1102,18 @@ func (f *file) appendLintErrors(source string, err error) bool {
10951102
}
10961103

10971104
for _, annotation := range annotations.FileAnnotations() {
1105+
// Convert 1-indexed byte-based line/column to byte offset.
1106+
startLocation := f.file.InverseLocation(annotation.StartLine(), annotation.StartColumn(), positionalEncoding)
1107+
endLocation := f.file.InverseLocation(annotation.EndLine(), annotation.EndColumn(), positionalEncoding)
1108+
protocolRange := reportLocationsToProtocolRange(startLocation, endLocation)
10981109
f.diagnostics = append(f.diagnostics, protocol.Diagnostic{
1099-
Range: protocol.Range{
1100-
Start: protocol.Position{
1101-
Line: uint32(annotation.StartLine()) - 1,
1102-
Character: uint32(annotation.StartColumn()) - 1,
1103-
},
1104-
End: protocol.Position{
1105-
Line: uint32(annotation.EndLine()) - 1,
1106-
Character: uint32(annotation.EndColumn()) - 1,
1107-
},
1108-
},
1110+
Range: protocolRange,
11091111
Code: annotation.Type(),
11101112
Severity: protocol.DiagnosticSeverityWarning,
11111113
Source: source,
11121114
Message: annotation.Message(),
11131115
})
11141116
}
1115-
11161117
return true
11171118
}
11181119

@@ -1149,3 +1150,11 @@ func (f *file) PublishDiagnostics(ctx context.Context) {
11491150
type wktObjectInfo struct {
11501151
storage.ObjectInfo
11511152
}
1153+
1154+
func readAllAsString(reader io.Reader) (string, error) {
1155+
var builder strings.Builder
1156+
if _, err := io.Copy(&builder, reader); err != nil {
1157+
return "", err
1158+
}
1159+
return builder.String(), nil
1160+
}

private/buf/buflsp/image.go

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"buf.build/go/standard/xslices"
2525
"github.com/bufbuild/buf/private/bufpkg/bufimage"
2626
"github.com/bufbuild/protocompile"
27+
"github.com/bufbuild/protocompile/experimental/report"
2728
"github.com/bufbuild/protocompile/linker"
2829
"github.com/bufbuild/protocompile/parser"
2930
"github.com/bufbuild/protocompile/protoutil"
@@ -75,7 +76,7 @@ func buildImage(
7576
logger.Warn("error building image", slog.String("path", path), xslog.ErrorAttr(err))
7677
if len(errorsWithPos) > 0 {
7778
diagnostics = xslices.Map(errorsWithPos, func(errorWithPos reporter.ErrorWithPos) protocol.Diagnostic {
78-
return newDiagnostic(errorWithPos, false)
79+
return newDiagnostic(errorWithPos, false, opener, logger)
7980
})
8081
}
8182
}
@@ -178,10 +179,40 @@ func buildImage(
178179
//
179180
// Unfortunately, protocompile's errors are currently too meagre to provide full code
180181
// spans; that will require a fix in the compiler.
181-
func newDiagnostic(err reporter.ErrorWithPos, isWarning bool) protocol.Diagnostic {
182+
func newDiagnostic(err reporter.ErrorWithPos, isWarning bool, opener fileOpener, logger *slog.Logger) protocol.Diagnostic {
183+
// Read the file text for the error's filename to convert byte offset to UTF-16 column.
184+
position := err.GetPosition()
185+
filename := position.Filename
186+
// Fallback to byte-based column (will be wrong for non-ASCII).
187+
utf16Col := err.GetPosition().Col - 1
188+
189+
// TODO: this is a temporary workaround for old diagnostic errors.
190+
// When using the new compiler these conversions will be already handled.
191+
if rc, openErr := opener(filename); openErr == nil {
192+
defer rc.Close()
193+
text, readErr := readAllAsString(rc)
194+
if readErr == nil {
195+
file := report.NewFile(filename, text)
196+
loc := file.Location(position.Offset, positionalEncoding)
197+
utf16Col = loc.Column - 1
198+
} else {
199+
logger.Warn(
200+
"failed to read file for diagnostic position encoding",
201+
slog.String("filename", filename),
202+
xslog.ErrorAttr(readErr),
203+
)
204+
}
205+
} else {
206+
logger.Warn(
207+
"failed to open file for diagnostic position encoding",
208+
slog.String("filename", filename),
209+
xslog.ErrorAttr(openErr),
210+
)
211+
}
212+
182213
pos := protocol.Position{
183214
Line: uint32(err.GetPosition().Line - 1),
184-
Character: uint32(err.GetPosition().Col - 1),
215+
Character: uint32(utf16Col),
185216
}
186217

187218
severity := protocol.DiagnosticSeverityError

private/buf/buflsp/server.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ func (s *server) Formatting(
289289
warningErrorsWithPos = append(warningErrorsWithPos, errorWithPos)
290290
},
291291
))
292-
parsed, err := parser.Parse(file.uri.Filename(), strings.NewReader(file.text), handler)
292+
parsed, err := parser.Parse(file.uri.Filename(), strings.NewReader(file.file.Text()), handler)
293293
if err == nil {
294294
_, _ = parser.ResultFromAST(parsed, true, handler)
295295
}
@@ -306,14 +306,14 @@ func (s *server) Formatting(
306306
return nil, err
307307
}
308308
newText := out.String()
309-
if newText == file.text {
309+
if newText == file.file.Text() {
310310
return nil, nil
311311
}
312312

313313
// Calculate the end location for the file range.
314-
endLine := strings.Count(file.text, "\n")
314+
endLine := strings.Count(file.file.Text(), "\n")
315315
endCharacter := 0
316-
for _, char := range file.text[strings.LastIndexByte(file.text, '\n')+1:] {
316+
for _, char := range file.file.Text()[strings.LastIndexByte(file.file.Text(), '\n')+1:] {
317317
endCharacter += utf16.RuneLen(char)
318318
}
319319
return []protocol.TextEdit{

private/buf/buflsp/symbol.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ func (s *symbol) FormatDocs(ctx context.Context) string {
175175
case *imported:
176176
imported, _ := s.kind.(*imported)
177177
// Provide a preview of the imported file.
178-
return fmt.Sprintf("```proto\n%s\n```", imported.file.text)
178+
return fmt.Sprintf("```proto\n%s\n```", imported.file.file.Text())
179179
case *tag:
180180
plural := func(i int) string {
181181
if i == 1 {
@@ -398,6 +398,10 @@ func positionInRange(position protocol.Position, within protocol.Range) int {
398398
func reportSpanToProtocolRange(span report.Span) protocol.Range {
399399
startLocation := span.File.Location(span.Start, positionalEncoding)
400400
endLocation := span.File.Location(span.End, positionalEncoding)
401+
return reportLocationsToProtocolRange(startLocation, endLocation)
402+
}
403+
404+
func reportLocationsToProtocolRange(startLocation, endLocation report.Location) protocol.Range {
401405
return protocol.Range{
402406
Start: protocol.Position{
403407
Line: uint32(startLocation.Line - 1),

0 commit comments

Comments
 (0)