Skip to content

Commit 4212b78

Browse files
authored
LSP cache file imports on the workspace (#4116)
1 parent 5906a3a commit 4212b78

File tree

4 files changed

+147
-130
lines changed

4 files changed

+147
-130
lines changed

private/buf/buflsp/completion.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ func completionItemsForField(ctx context.Context, file *file, declPath []ast.Dec
405405
//
406406
// Suggest all importable files.
407407
func completionItemsForImport(ctx context.Context, file *file, declImport ast.DeclImport, position protocol.Position) []protocol.CompletionItem {
408-
file.lsp.logger.DebugContext(ctx, "completion: import declaration", slog.Int("importable_count", len(file.importToFile)))
408+
file.lsp.logger.DebugContext(ctx, "completion: import declaration", slog.Int("importable_count", len(file.workspace.PathToFile())))
409409

410410
positionLocation := file.file.InverseLocation(int(position.Line)+1, int(position.Character)+1, positionalEncoding)
411411
offset := positionLocation.Offset
@@ -457,7 +457,11 @@ func completionItemsForImport(ctx context.Context, file *file, declImport ast.De
457457
}
458458

459459
var items []protocol.CompletionItem
460-
for importPath, importFile := range file.importToFile {
460+
for importPath, importFile := range file.workspace.PathToFile() {
461+
if file == importFile {
462+
continue // ignore self
463+
}
464+
461465
suggest := fmt.Sprintf("%q", importPath)
462466
if !strings.HasPrefix(suggest, prefix) || !strings.HasSuffix(suggest, suffix) {
463467
file.lsp.logger.Debug("completion: skipping on prefix/suffix",
@@ -601,7 +605,10 @@ func typeReferencesToCompletionItems(
601605
offset int,
602606
) iter.Seq[protocol.CompletionItem] {
603607
fileSymbolTypesIter := func(yield func(*file, *symbol) bool) {
604-
for _, imported := range current.importToFile {
608+
for _, imported := range current.workspace.PathToFile() {
609+
if imported == current {
610+
continue
611+
}
605612
for _, symbol := range imported.referenceableSymbols {
606613
if !yield(imported, symbol) {
607614
return

private/buf/buflsp/file.go

Lines changed: 39 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -24,26 +24,23 @@ import (
2424
"io/fs"
2525
"iter"
2626
"log/slog"
27-
"os"
2827
"slices"
2928
"strings"
3029

31-
"buf.build/go/standard/xio"
3230
"buf.build/go/standard/xlog/xslog"
3331
"buf.build/go/standard/xslices"
3432
"github.com/bufbuild/buf/private/bufpkg/bufanalysis"
3533
"github.com/bufbuild/buf/private/bufpkg/bufcheck"
3634
"github.com/bufbuild/buf/private/bufpkg/bufimage"
35+
"github.com/bufbuild/buf/private/bufpkg/bufmodule"
3736
"github.com/bufbuild/buf/private/pkg/storage"
3837
"github.com/bufbuild/protocompile/experimental/ast/predeclared"
3938
"github.com/bufbuild/protocompile/experimental/incremental"
4039
"github.com/bufbuild/protocompile/experimental/incremental/queries"
4140
"github.com/bufbuild/protocompile/experimental/ir"
4241
"github.com/bufbuild/protocompile/experimental/report"
4342
"github.com/bufbuild/protocompile/experimental/seq"
44-
"github.com/bufbuild/protocompile/experimental/source"
4543
"go.lsp.dev/protocol"
46-
"go.lsp.dev/uri"
4744
)
4845

4946
// file is a file that has been opened by the client.
@@ -60,10 +57,8 @@ type file struct {
6057
version int32
6158
hasText bool // Whether this file has ever had text read into it.
6259

63-
workspace *workspace // May be nil.
64-
65-
objectInfo storage.ObjectInfo
66-
importToFile map[string]*file
60+
workspace *workspace // May be nil.
61+
objectInfo storage.ObjectInfo // Info in the context of the workspace.
6762

6863
ir ir.File
6964
referenceableSymbols map[string]*symbol
@@ -102,10 +97,6 @@ func (f *file) Reset(ctx context.Context) {
10297
f.diagnostics = nil
10398
f.symbols = nil
10499
f.image = nil
105-
for _, imported := range f.importToFile {
106-
imported.Close(ctx)
107-
}
108-
f.importToFile = nil
109100
}
110101

111102
// Close marks a file as closed.
@@ -126,28 +117,39 @@ func (f *file) Close(ctx context.Context) {
126117
// Some files may be opened as dependencies, so we want to avoid doing extra
127118
// work like sending progress notifications.
128119
func (f *file) IsOpenInEditor() bool {
129-
return f.version != -1 // See [file.ReadFromDisk].
120+
return f.version != -1 // See [file.ReadFromWorkspace].
130121
}
131122

132-
// ReadFromDisk reads this file from disk if it has never had data loaded into it before.
123+
// ReadFromWorkspace reads this file from the workspace if it has never had data loaded into it
124+
// before.
133125
//
134-
// If it has been read from disk before, or has received updates from the LSP client, this
135-
// function returns nil.
136-
func (f *file) ReadFromDisk(ctx context.Context) (err error) {
126+
// If it has been read from disk before, or has received updates from the LSP client, this function
127+
// returns nil.
128+
func (f *file) ReadFromWorkspace(ctx context.Context) (err error) {
137129
if f.hasText {
138130
return nil
139131
}
140132

141133
fileName := f.uri.Filename()
142-
reader, err := os.Open(fileName)
134+
var reader io.ReadCloser
135+
switch info := f.objectInfo.(type) {
136+
case bufmodule.FileInfo:
137+
reader, err = info.Module().GetFile(ctx, info.Path())
138+
case wktObjectInfo:
139+
reader, err = f.lsp.wktBucket.Get(ctx, info.Path())
140+
default:
141+
return fmt.Errorf("unsupported objectInfo type %T", f.objectInfo)
142+
}
143143
if err != nil {
144-
return fmt.Errorf("could not open file %q from disk: %w", f.uri, err)
144+
return fmt.Errorf("read file %q from workspace", err)
145145
}
146146
defer reader.Close()
147-
text, err := readAllAsString(reader)
148-
if err != nil {
149-
return fmt.Errorf("could not read file %q from disk: %w", f.uri, err)
147+
148+
var builder strings.Builder
149+
if _, err := io.Copy(&builder, reader); err != nil {
150+
return fmt.Errorf("could not read file %q from workspace", err)
150151
}
152+
text := builder.String()
151153

152154
f.version = -1
153155
f.file = report.NewFile(fileName, text)
@@ -176,19 +178,16 @@ func (f *file) Refresh(ctx context.Context) {
176178
}
177179
progress.Begin(ctx, "Indexing")
178180

179-
progress.Report(ctx, "Setting workspace", 1.0/5)
181+
progress.Report(ctx, "Setting workspace", 1.0/4)
180182
f.RefreshWorkspace(ctx)
181183

182-
progress.Report(ctx, "Indexing imports", 2.0/5)
183-
f.IndexImports(ctx)
184-
185-
progress.Report(ctx, "Parsing IR", 3.0/5)
184+
progress.Report(ctx, "Parsing IR", 2.0/4)
186185
f.RefreshIR(ctx)
187186

188-
progress.Report(ctx, "Indexing Symbols", 4.0/5)
187+
progress.Report(ctx, "Indexing Symbols", 3.0/4)
189188
f.IndexSymbols(ctx)
190189

191-
progress.Report(ctx, "Running Checks", 5.0/5)
190+
progress.Report(ctx, "Running Checks", 4.0/4)
192191
if f.IsOpenInEditor() {
193192
f.BuildImage(ctx)
194193
f.RunLints(ctx)
@@ -233,75 +232,6 @@ func (f *file) RefreshWorkspace(ctx context.Context) {
233232
}
234233
}
235234

236-
// IndexImports keeps track of importable files.
237-
//
238-
// This operation requires RefreshWorkspace.
239-
func (f *file) IndexImports(ctx context.Context) {
240-
defer xslog.DebugProfile(f.lsp.logger, slog.String("uri", string(f.uri)))
241-
if f.importToFile != nil {
242-
return
243-
}
244-
fileInfos, err := f.getWorkspaceFileInfos(ctx)
245-
if err != nil {
246-
f.lsp.logger.Error(
247-
"failed to get importable files",
248-
slog.String("file", f.uri.Filename()),
249-
)
250-
}
251-
f.importToFile = make(map[string]*file)
252-
for _, importable := range fileInfos {
253-
if importable.ExternalPath() == f.uri.Filename() {
254-
f.objectInfo = importable
255-
if err := f.ReadFromDisk(ctx); err != nil {
256-
f.lsp.logger.Error(
257-
"failed to read contents for file",
258-
xslog.ErrorAttr(err),
259-
slog.String("file", importable.Path()),
260-
)
261-
}
262-
continue
263-
}
264-
importableFile := f.Manager().Track(uri.File(importable.LocalPath()))
265-
if importableFile.objectInfo == nil {
266-
importableFile.objectInfo = importable
267-
}
268-
if err := importableFile.ReadFromDisk(ctx); err != nil {
269-
f.lsp.logger.Error(
270-
"failed to read contents for file",
271-
xslog.ErrorAttr(err),
272-
slog.String("file", importable.Path()),
273-
)
274-
}
275-
f.importToFile[importableFile.objectInfo.Path()] = importableFile
276-
}
277-
}
278-
279-
// getWorkspaceFileInfos returns all files within the workspace.
280-
//
281-
// Note that this performs no validation on these files, because those files might be open in the
282-
// editor and might contain invalid syntax at the moment. We only want to get their paths and nothing
283-
// more.
284-
//
285-
// This operation requires RefreshWorkspace.
286-
func (f *file) getWorkspaceFileInfos(ctx context.Context) ([]storage.ObjectInfo, error) {
287-
seen := make(map[string]struct{})
288-
var fileInfos []storage.ObjectInfo
289-
for fileInfo := range f.workspace.FileInfo() {
290-
fileInfos = append(fileInfos, fileInfo)
291-
seen[fileInfo.Path()] = struct{}{}
292-
}
293-
// Add all wellknown types if not provided within the workspace.
294-
if err := f.lsp.wktBucket.Walk(ctx, "", func(objectInfo storage.ObjectInfo) error {
295-
if _, ok := seen[objectInfo.Path()]; !ok {
296-
fileInfos = append(fileInfos, wktObjectInfo{objectInfo})
297-
}
298-
return nil
299-
}); err != nil {
300-
return nil, err
301-
}
302-
return fileInfos, nil
303-
}
304-
305235
// RefreshIR queries for the IR of the file and the IR of each import file.
306236
// Diagnostics from the compiler are returned when applicable.
307237
//
@@ -313,19 +243,11 @@ func (f *file) RefreshIR(ctx context.Context) {
313243
slog.Int("version", int(f.version)),
314244
)
315245

316-
openerMap := map[string]string{
317-
f.objectInfo.Path(): f.file.Text(),
318-
}
319-
files := []*file{f}
320-
for path, file := range f.importToFile {
321-
openerMap[path] = file.file.Text()
322-
files = append(files, file)
323-
}
324-
opener := source.NewMap(openerMap)
246+
files := xslices.MapValuesToSlice(f.workspace.PathToFile())
325247
session := new(ir.Session)
326248
queries := xslices.Map(files, func(file *file) incremental.Query[ir.File] {
327249
return queries.IR{
328-
Opener: opener,
250+
Opener: f.workspace,
329251
Path: file.objectInfo.Path(),
330252
Session: session,
331253
}
@@ -346,7 +268,7 @@ func (f *file) RefreshIR(ctx context.Context) {
346268
}
347269
for i, file := range files {
348270
file.ir = results[i].Value
349-
if i > 0 {
271+
if f != file {
350272
// Update symbols for imports.
351273
file.IndexSymbols(ctx)
352274
}
@@ -420,7 +342,7 @@ func (f *file) IndexSymbols(ctx context.Context) {
420342
)
421343
continue
422344
}
423-
file, ok := f.importToFile[ref.def.Span().Path()]
345+
file, ok := f.workspace.PathToFile()[ref.def.Span().Path()]
424346
if !ok {
425347
// Check current file
426348
if ref.def.Span().Path() != f.objectInfo.Path() {
@@ -451,7 +373,10 @@ func (f *file) IndexSymbols(ctx context.Context) {
451373
}
452374

453375
// Resolve all references outside of this file to symbols in this file
454-
for _, file := range f.importToFile {
376+
for _, file := range f.workspace.PathToFile() {
377+
if f == file {
378+
continue // ignore self
379+
}
455380
for _, sym := range file.referenceSymbols {
456381
ref, ok := sym.kind.(*reference)
457382
if !ok {
@@ -703,7 +628,7 @@ func (f *file) importToSymbol(imp ir.Import) *symbol {
703628
file: f,
704629
span: imp.Decl.Span(),
705630
kind: &imported{
706-
file: f.importToFile[imp.File.Path()],
631+
file: f.workspace.PathToFile()[imp.File.Path()],
707632
},
708633
}
709634
}
@@ -767,12 +692,12 @@ func (f *file) newFileOpener() fileOpener {
767692
if f.objectInfo.Path() == path {
768693
file = f
769694
} else {
770-
file = f.importToFile[path]
695+
file = f.workspace.PathToFile()[path]
771696
}
772697
if file == nil {
773698
return nil, fmt.Errorf("%s: %w", path, fs.ErrNotExist)
774699
}
775-
return xio.CompositeReadCloser(strings.NewReader(file.file.Text()), xio.NopCloser), nil
700+
return io.NopCloser(strings.NewReader(file.file.Text())), nil
776701
}
777702
}
778703

@@ -897,14 +822,6 @@ type wktObjectInfo struct {
897822
storage.ObjectInfo
898823
}
899824

900-
func readAllAsString(reader io.Reader) (string, error) {
901-
var builder strings.Builder
902-
if _, err := io.Copy(&builder, reader); err != nil {
903-
return "", err
904-
}
905-
return builder.String(), nil
906-
}
907-
908825
// GetSymbols retrieves symbols for the file. If a query is passed, matches only symbols matching
909826
// the case-insensitive substring match to the symbol.
910827
//

private/buf/buflsp/image.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io"
2121
"log/slog"
22+
"strings"
2223

2324
"buf.build/go/standard/xlog/xslog"
2425
"buf.build/go/standard/xslices"
@@ -190,9 +191,9 @@ func newDiagnostic(err reporter.ErrorWithPos, isWarning bool, opener fileOpener,
190191
// When using the new compiler these conversions will be already handled.
191192
if rc, openErr := opener(filename); openErr == nil {
192193
defer rc.Close()
193-
text, readErr := readAllAsString(rc)
194-
if readErr == nil {
195-
file := report.NewFile(filename, text)
194+
var builder strings.Builder
195+
if _, readErr := io.Copy(&builder, rc); readErr == nil {
196+
file := report.NewFile(filename, builder.String())
196197
loc := file.Location(position.Offset, positionalEncoding)
197198
utf16Col = loc.Column - 1
198199
} else {

0 commit comments

Comments
 (0)