diff --git a/lsp/cache/invalidation_test.go b/lsp/cache/invalidation_test.go index 1399f39..c949c3b 100644 --- a/lsp/cache/invalidation_test.go +++ b/lsp/cache/invalidation_test.go @@ -71,11 +71,8 @@ func newViewHarness(t *testing.T, files []*FileChange) *viewHarness { view := NewView("file:///tmp", fs, nil, options.Patch{}) - ss, release := view.Snapshot() - defer release() - for _, f := range files { - if _, err := ss.Parse(t.Context(), f.URI); err != nil { + if _, err := view.Parse(t.Context(), f.URI); err != nil { t.Fatal(err) } } @@ -95,7 +92,7 @@ func (h *viewHarness) change(t *testing.T, change *FileChange) []uri.URI { done := make(chan []uri.URI, 1) - h.view.FileChange(t.Context(), []*FileChange{change}, func(a []uri.URI) { + h.view.FileChange(t.Context(), []*FileChange{change}, func(_ uint64, a []uri.URI) { done <- a }) @@ -109,15 +106,6 @@ func (h *viewHarness) change(t *testing.T, change *FileChange) []uri.URI { } } -func (h *viewHarness) snapshot(t *testing.T) *Snapshot { - t.Helper() - - ss, release := h.view.Snapshot() - t.Cleanup(release) - - return ss -} - func Test_FileChangeInvalidatesDependents(t *testing.T) { for _, tt := range []struct { name string @@ -200,9 +188,8 @@ struct Gundam { } // Changed content is visible through the view's store. - ss := h.snapshot(t) for file, marker := range tt.wantFresh { - pf, err := ss.Parse(t.Context(), file) + pf, err := h.view.Parse(t.Context(), file) require.NoError(t, err) assert.Contains(t, pf.Tokens(), marker, diff --git a/lsp/cache/parse_test.go b/lsp/cache/parse_test.go index 13d38b4..e828f1f 100644 --- a/lsp/cache/parse_test.go +++ b/lsp/cache/parse_test.go @@ -63,7 +63,7 @@ struct Xtruct3 // TestParsedFileDefinitions pins the definition and enum-value indexes: // every top-level definition is reachable by name, enum values by name. func TestParsedFileDefinitions(t *testing.T) { - ss := BuildSnapshotForTest([]*FileChange{ + ss := BuildViewForTest([]*FileChange{ {URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(`struct S { 1: required string Name, } diff --git a/lsp/cache/resolver_test.go b/lsp/cache/resolver_test.go index bae26c5..17dd032 100644 --- a/lsp/cache/resolver_test.go +++ b/lsp/cache/resolver_test.go @@ -32,11 +32,9 @@ func TestResolver(t *testing.T) { c := New() fs := NewOverlayFS(c) - view := NewView(uri.File(tmpDir), fs, nil, options.Patch{}) - includePaths := []string{sharedDir} - ss := NewSnapshot(view, includePaths) + view := NewView(uri.File(tmpDir), fs, []string{sharedDir}, options.Patch{}) - resolver := ss.Resolver() + resolver := view.Resolver() for _, tt := range []struct { name string diff --git a/lsp/cache/session.go b/lsp/cache/session.go index 203d6ed..1785a64 100644 --- a/lsp/cache/session.go +++ b/lsp/cache/session.go @@ -18,8 +18,8 @@ type Session struct { views []*View viewMap map[uri.URI]*View // map of URI->best view - // session holds overlayFS to manage file content - // view, snapshot only holds FileSource to read from overlayFS + // The session owns the overlayFS: open-editor content lives here, and + // views read through it. *overlayFS } diff --git a/lsp/cache/snapshot.go b/lsp/cache/snapshot.go index 62e5e8a..61ad699 100644 --- a/lsp/cache/snapshot.go +++ b/lsp/cache/snapshot.go @@ -14,79 +14,6 @@ import ( "github.com/karitham/thrift-ls/syntax" ) -// Snapshot is a read handle over a view's current state. It exists so -// request handlers have a stable value to pass around; all reads see the -// view's live store, so nothing is copied or frozen. -// -// The gen field pins the generation the handle was taken at, for IsCurrent -// staleness checks. -type Snapshot struct { - view *View - includePaths []string - gen uint64 -} - -// Snapshot returns a read handle over the view's current state plus a -// no-op release function. -func (v *View) Snapshot() (*Snapshot, func()) { - ss := &Snapshot{ - view: v, - includePaths: v.includePaths, - gen: v.Generation(), - } - - return ss, func() {} -} - -// NewSnapshot returns a read handle over view, carrying an includePaths -// override for the handle's resolver. -func NewSnapshot(view *View, includePaths []string) *Snapshot { - return &Snapshot{ - view: view, - includePaths: includePaths, - gen: view.Generation(), - } -} - -func (s *Snapshot) Includes(file uri.URI) []uri.URI { - return s.view.Includes(file) -} - -func (s *Snapshot) Includers(file uri.URI) []uri.URI { - return s.view.Includers(file) -} - -func (s *Snapshot) Dependents(uri uri.URI) []uri.URI { - return s.view.Dependents(uri) -} - -// View returns the view this snapshot serves: the workspace folder the -// snapshot resolves files under. -func (s *Snapshot) View() *View { - return s.view -} - -// Resolver returns a new Resolver instance for this snapshot. -// The resolver provides centralized include path resolution. -func (s *Snapshot) Resolver() *Resolver { - return newResolver(s.includePaths, s.view.fs) -} - -func (s *Snapshot) ReadFile(ctx context.Context, uri uri.URI) (FileHandle, error) { - return s.view.ReadFile(ctx, uri) -} - -func (s *Snapshot) Parse(ctx context.Context, uri uri.URI) (*ParsedFile, error) { - return s.view.Parse(ctx, uri) -} - -// TokensForFile returns the identifier tokens of file and its transitively -// included files. Each file's token set is computed once per parse and -// reused, so typing does not re-walk the include closure's ASTs. -func (s *Snapshot) TokensForFile(file uri.URI) map[string]struct{} { - return s.view.TokensForFile(file) -} - // Resolver provides centralized include path resolution. type Resolver struct { includePaths []string @@ -102,7 +29,7 @@ func newResolver(includePaths []string, src FileSource) *Resolver { } } -// IncludePaths returns the include paths configured for this snapshot +// IncludePaths returns the include paths configured for this resolver. func (r *Resolver) IncludePaths() []string { return r.includePaths } @@ -215,13 +142,13 @@ type viewFile struct { func (f *viewFile) Stat() (fs.FileInfo, error) { return f.info, nil } func (f *viewFile) Close() error { return nil } -func BuildSnapshotForTest(files []*FileChange) *Snapshot { - return BuildSnapshotForTestWithPaths(nil, files) +func BuildViewForTest(files []*FileChange) *View { + return BuildViewForTestWithPaths(nil, files) } -// BuildSnapshotForTestWithPaths is BuildSnapshotForTest with configured -// include paths, for cross-project include resolution tests. -func BuildSnapshotForTestWithPaths(includePaths []string, files []*FileChange) *Snapshot { +// BuildViewForTestWithPaths is BuildViewForTest with configured include +// paths, for cross-project include resolution tests. +func BuildViewForTestWithPaths(includePaths []string, files []*FileChange) *View { c := New() fs := NewOverlayFS(c) _ = fs.Update(context.TODO(), files) @@ -232,7 +159,5 @@ func BuildSnapshotForTestWithPaths(includePaths []string, files []*FileChange) * _, _ = view.Parse(context.TODO(), f.URI) } - ss, _ := view.Snapshot() - - return ss + return view } diff --git a/lsp/cache/view.go b/lsp/cache/view.go index 390620e..b01ecdd 100644 --- a/lsp/cache/view.go +++ b/lsp/cache/view.go @@ -317,21 +317,20 @@ func (v *View) Generation() uint64 { return v.gen.Load() } -// IsCurrent reports whether ss was taken from the latest generation of the -// view. Used by asynchronous work to drop results that a newer change -// superseded. -func (v *View) IsCurrent(ss *Snapshot) bool { - return ss.gen == ss.view.Generation() +// IsCurrent reports whether gen is still the view's latest generation. +// Used by asynchronous work to drop results that a newer change superseded. +func (v *View) IsCurrent(gen uint64) bool { + return v.Generation() == gen } // FileChange applies changes to the view: it invalidates the changed files' // entries, re-parses them so their include edges are fresh before requests -// observe the change, then runs postFns asynchronously with the affected -// URIs (changed files plus their transitive dependents). The request thread -// never blocks on postFns, so diagnostics-heavy work does not stall the -// editor. A concurrent generation check lets stale postFn results be -// dropped. -func (v *View) FileChange(ctx context.Context, changes []*FileChange, postFns ...func(affected []uri.URI)) { +// observe the change, then runs postFns asynchronously with the generation +// of this change and the affected URIs (changed files plus their transitive +// dependents). The request thread never blocks on postFns, so +// diagnostics-heavy work does not stall the editor. A postFn whose +// generation is no longer current can drop its results. +func (v *View) FileChange(ctx context.Context, changes []*FileChange, postFns ...func(gen uint64, affected []uri.URI)) { uris := make([]uri.URI, 0, len(changes)) for _, change := range changes { uris = append(uris, change.URI) @@ -360,11 +359,11 @@ func (v *View) FileChange(ctx context.Context, changes []*FileChange, postFns .. affected := v.affected(uris) - v.gen.Add(1) + gen := v.gen.Add(1) go func() { for i := range postFns { - postFns[i](affected) + postFns[i](gen, affected) } }() } diff --git a/lsp/cache/view_test.go b/lsp/cache/view_test.go index e87121a..ab5a0cd 100644 --- a/lsp/cache/view_test.go +++ b/lsp/cache/view_test.go @@ -160,8 +160,7 @@ func Test_View_GenerationAndIsCurrent(t *testing.T) { h := newViewHarness(t, gundamFiles()) before := h.view.Generation() - ss := h.snapshot(t) - assert.True(t, h.view.IsCurrent(ss)) + assert.True(t, h.view.IsCurrent(before)) h.change(t, &FileChange{ URI: uri.URI(federation), @@ -171,16 +170,14 @@ func Test_View_GenerationAndIsCurrent(t *testing.T) { }) assert.Greater(t, h.view.Generation(), before, "FileChange bumps the generation") - assert.False(t, h.view.IsCurrent(ss), "a handle from an older generation is not current") - - fresh := h.snapshot(t) - assert.True(t, h.view.IsCurrent(fresh)) + assert.False(t, h.view.IsCurrent(before), "a generation older than the latest is not current") + assert.True(t, h.view.IsCurrent(h.view.Generation())) } -// Test_SnapshotParseIncludeCycles exercises include cycles through the full +// Test_ViewParseIncludeCycles exercises include cycles through the full // parse path: parsing registers edges, and the graph must settle without // infinite recursion. -func Test_SnapshotParseIncludeCycles(t *testing.T) { +func Test_ViewParseIncludeCycles(t *testing.T) { dir := t.TempDir() char := uri.File(filepath.Join(dir, "char.thrift")) amuro := uri.File(filepath.Join(dir, "amuro.thrift")) @@ -192,7 +189,7 @@ func Test_SnapshotParseIncludeCycles(t *testing.T) { {URI: self, Content: []byte(`include "side_effect.thrift"`), From: FileChangeTypeDidOpen}, } - ss := BuildSnapshotForTest(files) + ss := BuildViewForTest(files) // both directions of the mutual cycle are recorded assert.Equal(t, []uri.URI{amuro}, ss.Includes(char)) diff --git a/lsp/codeaction.go b/lsp/codeaction.go index c3f4e1a..014e276 100644 --- a/lsp/codeaction.go +++ b/lsp/codeaction.go @@ -15,10 +15,10 @@ import ( // also offered as a quickfix. Actions are filtered to the kinds the client // requested. func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CommandOrCodeAction, error) { - return withFile(ctx, s.session, params.TextDocument.URI, func(ss *cache.Snapshot, fh cache.FileHandle) ([]protocol.CommandOrCodeAction, error) { + return withFile(ctx, s.session, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.CommandOrCodeAction, error) { var actions []protocol.CodeAction - enum, err := source.MakeEnumValuesExplicitAction(ctx, ss, fh, params.Range) + enum, err := source.MakeEnumValuesExplicitAction(ctx, view, fh, params.Range) if err != nil { return nil, err } @@ -33,13 +33,13 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara actions = append(actions, *enum) } - fieldActions, err := source.MakeFieldQualifierAction(ctx, ss, fh, params.Range) + fieldActions, err := source.MakeFieldQualifierAction(ctx, view, fh, params.Range) if err != nil { return nil, err } actions = append(actions, fieldActions...) - removeInclude, err := source.MakeRemoveUnusedIncludeAction(ctx, ss, fh, params.Range, params.Context.Diagnostics) + removeInclude, err := source.MakeRemoveUnusedIncludeAction(ctx, view, fh, params.Range, params.Context.Diagnostics) if err != nil { return nil, err } @@ -47,7 +47,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara actions = append(actions, *removeInclude) } - addInclude, err := source.MakeAddMissingIncludeAction(ctx, ss, fh, params.Range, params.Context.Diagnostics) + addInclude, err := source.MakeAddMissingIncludeAction(ctx, view, fh, params.Range, params.Context.Diagnostics) if err != nil { return nil, err } diff --git a/lsp/codejump.go b/lsp/codejump.go index b62693f..d5db071 100644 --- a/lsp/codejump.go +++ b/lsp/codejump.go @@ -10,19 +10,19 @@ import ( ) func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) (result []protocol.Location, err error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) ([]protocol.Location, error) { - return source.Definition(ctx, ss, params.TextDocument.URI, params.Position) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) ([]protocol.Location, error) { + return source.Definition(ctx, view, params.TextDocument.URI, params.Position) }) } func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) (result []protocol.Location, err error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) ([]protocol.Location, error) { - return source.Reference(ctx, ss, params.TextDocument.URI, params.Position) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) ([]protocol.Location, error) { + return source.Reference(ctx, view, params.TextDocument.URI, params.Position) }) } func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) (result []protocol.Location, err error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) ([]protocol.Location, error) { - return source.TypeDefinition(ctx, ss, params.TextDocument.URI, params.Position) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) ([]protocol.Location, error) { + return source.TypeDefinition(ctx, view, params.TextDocument.URI, params.Position) }) } diff --git a/lsp/diagnostic.go b/lsp/diagnostic.go index a5fcf3f..bac83aa 100644 --- a/lsp/diagnostic.go +++ b/lsp/diagnostic.go @@ -12,7 +12,7 @@ import ( "github.com/karitham/thrift-ls/lsp/source" ) -func (s *Server) diagnostic(ctx context.Context, ss *cache.Snapshot, file uri.URI) error { +func (s *Server) diagnostic(ctx context.Context, view *cache.View, file uri.URI) error { if s.client == nil { return nil } @@ -22,7 +22,7 @@ func (s *Server) diagnostic(ctx context.Context, ss *cache.Snapshot, file uri.UR diag := source.NewDiagnostic() - diagRes, err := diag.Diagnostic(ctx, ss, []uri.URI{file}) + diagRes, err := diag.Diagnostic(ctx, view, []uri.URI{file}) if err != nil { logError("diagnostic failed", err) } diff --git a/lsp/didchange_test.go b/lsp/didchange_test.go index 4345ba0..7046819 100644 --- a/lsp/didchange_test.go +++ b/lsp/didchange_test.go @@ -195,12 +195,9 @@ exception BayFull { view, err := srv.session.ViewOf(aURI) assert.NoError(t, err) - ss, release := view.Snapshot() - defer release() - - _, err = ss.Parse(ctx, aURI) + _, err = view.Parse(ctx, aURI) assert.NoError(t, err) - assert.Equal(t, []uri.URI{aURI}, ss.Dependents(bURI)) + assert.Equal(t, []uri.URI{aURI}, view.Dependents(bURI)) } // Test_DidChangeWatchedFilesRefreshesDiskContent: disk events outside the diff --git a/lsp/folding.go b/lsp/folding.go index 2ad1cd2..729d01c 100644 --- a/lsp/folding.go +++ b/lsp/folding.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) foldingRanges(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) ([]protocol.FoldingRange, error) { - return source.Ranges(ctx, ss, params.TextDocument.URI), nil + return withView(s.session, params.TextDocument.URI, func(view *cache.View) ([]protocol.FoldingRange, error) { + return source.Ranges(ctx, view, params.TextDocument.URI), nil }) } diff --git a/lsp/format.go b/lsp/format.go index d0714a6..13fc18c 100644 --- a/lsp/format.go +++ b/lsp/format.go @@ -10,8 +10,8 @@ import ( ) func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) (result []protocol.TextEdit, err error) { - return withFile(ctx, s.session, params.TextDocument.URI, func(ss *cache.Snapshot, fh cache.FileHandle) ([]protocol.TextEdit, error) { - edit, err := source.FormatDocument(ctx, ss, fh, s.formatOptions(ss.View())) + return withFile(ctx, s.session, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.TextEdit, error) { + edit, err := source.FormatDocument(ctx, view, fh, s.formatOptions(view)) if err != nil { return nil, err } @@ -25,7 +25,7 @@ func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormat } func (s *Server) rangeFormatting(ctx context.Context, params *protocol.DocumentRangeFormattingParams) (result []protocol.TextEdit, err error) { - return withFile(ctx, s.session, params.TextDocument.URI, func(ss *cache.Snapshot, fh cache.FileHandle) ([]protocol.TextEdit, error) { - return source.FormatRange(ctx, ss, fh, s.formatOptions(ss.View()), params.Range) + return withFile(ctx, s.session, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.TextEdit, error) { + return source.FormatRange(ctx, view, fh, s.formatOptions(view), params.Range) }) } diff --git a/lsp/highlight.go b/lsp/highlight.go index daf5417..350b0e5 100644 --- a/lsp/highlight.go +++ b/lsp/highlight.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) ([]protocol.DocumentHighlight, error) { - return source.Highlight(ctx, ss, params.TextDocument.URI, params.Position) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) ([]protocol.DocumentHighlight, error) { + return source.Highlight(ctx, view, params.TextDocument.URI, params.Position) }) } diff --git a/lsp/hover.go b/lsp/hover.go index 8693f51..216a2b0 100644 --- a/lsp/hover.go +++ b/lsp/hover.go @@ -11,8 +11,8 @@ import ( ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) (*protocol.Hover, error) { - content, err := source.Hover(ctx, ss, params.TextDocument.URI, params.Position) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) (*protocol.Hover, error) { + content, err := source.Hover(ctx, view, params.TextDocument.URI, params.Position) if err != nil { return nil, err } diff --git a/lsp/impl.go b/lsp/impl.go index 2a8c334..7ca8e8c 100644 --- a/lsp/impl.go +++ b/lsp/impl.go @@ -159,37 +159,34 @@ func (s *Server) watchedFileChange(ctx context.Context, event protocol.FileEvent } // postDiagnostics returns a FileChange postFn that publishes diagnostics for -// every affected file (changed files plus their transitive dependents) on the -// view's current snapshot. Diagnostics run in the background (FileChange -// invokes postFns asynchronously); if a newer change lands while the analysis -// runs, the results are dropped — the newer change publishes its own. -func (s *Server) postDiagnostics(ctx context.Context, view *cache.View) func([]uri.URI) { +// every affected file (changed files plus their transitive dependents). +// Diagnostics run in the background (FileChange invokes postFns +// asynchronously); if a newer change lands while the analysis runs, the +// generation check drops the results — the newer change publishes its own. +func (s *Server) postDiagnostics(ctx context.Context, view *cache.View) func(uint64, []uri.URI) { // The request context dies when the LSP request returns; the // diagnostics goroutine outlives it. ctx = context.WithoutCancel(ctx) - return func(affected []uri.URI) { - ss, release := view.Snapshot() - defer release() - - if !view.IsCurrent(ss) { + return func(gen uint64, affected []uri.URI) { + if !view.IsCurrent(gen) { return } - s.diagnose(ctx, ss, affected) + s.diagnose(ctx, view, affected) } } // diagnose publishes diagnostics for every affected file, in parallel: a -// change to a shared include re-diagnoses all its dependents, and the -// snapshot (and the client connection) are safe for concurrent reads and +// change to a shared include re-diagnoses all its dependents, and the view +// (and the client connection) are safe for concurrent reads and // notifications. -func (s *Server) diagnose(ctx context.Context, ss *cache.Snapshot, affected []uri.URI) { +func (s *Server) diagnose(ctx context.Context, view *cache.View, affected []uri.URI) { var wg sync.WaitGroup for _, file := range affected { wg.Go(func() { - if err := s.diagnostic(ctx, ss, file); err != nil { + if err := s.diagnostic(ctx, view, file); err != nil { logError("diagnostic error", err) } }) @@ -199,8 +196,8 @@ func (s *Server) diagnose(ctx context.Context, ss *cache.Snapshot, affected []ur } func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { - return withFile(ctx, s.session, params.TextDocument.URI, func(ss *cache.Snapshot, fh cache.FileHandle) (*protocol.CompletionList, error) { - items, rng, truncated, err := source.DefaultTokenCompletion.Completion(ctx, ss, &source.CompletionRequest{ + return withFile(ctx, s.session, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) (*protocol.CompletionList, error) { + items, rng, truncated, err := source.DefaultTokenCompletion.Completion(ctx, view, &source.CompletionRequest{ Pos: protocol.Position{ Line: params.Position.Line, Character: params.Position.Character, diff --git a/lsp/include_paths_test.go b/lsp/include_paths_test.go index 29819c3..f474d5d 100644 --- a/lsp/include_paths_test.go +++ b/lsp/include_paths_test.go @@ -46,16 +46,13 @@ func TestConfigFileIncludePaths(t *testing.T) { view, err := srv.session.ViewOf(app) require.NoError(t, err) - snapshot, release := view.Snapshot() - defer release() - // The config's include path is absolute, resolved against the // config file's directory, not the process CWD. - assert.Equal(t, []string{includeDir}, snapshot.Resolver().IncludePaths()) + assert.Equal(t, []string{includeDir}, view.Resolver().IncludePaths()) // Resolving an include that only exists in the configured include // path finds it there. - resolved := snapshot.Resolver().ResolveInclude(app, "shared.thrift") + resolved := view.Resolver().ResolveInclude(app, "shared.thrift") assert.Equal(t, uri.File(shared), resolved) }) } diff --git a/lsp/links.go b/lsp/links.go index 329333b..032598c 100644 --- a/lsp/links.go +++ b/lsp/links.go @@ -10,7 +10,7 @@ import ( ) func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) ([]protocol.DocumentLink, error) { - return source.Links(ctx, ss, params.TextDocument.URI), nil + return withView(s.session, params.TextDocument.URI, func(view *cache.View) ([]protocol.DocumentLink, error) { + return source.Links(ctx, view, params.TextDocument.URI), nil }) } diff --git a/lsp/rename.go b/lsp/rename.go index e03e6ea..9d62507 100644 --- a/lsp/rename.go +++ b/lsp/rename.go @@ -10,13 +10,13 @@ import ( ) func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) (*protocol.Range, error) { - return source.PrepareRename(ctx, ss, params.TextDocument.URI, params.Position) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) (*protocol.Range, error) { + return source.PrepareRename(ctx, view, params.TextDocument.URI, params.Position) }) } func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) (*protocol.WorkspaceEdit, error) { - return source.Rename(ctx, ss, params.TextDocument.URI, params.Position, params.NewName) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) (*protocol.WorkspaceEdit, error) { + return source.Rename(ctx, view, params.TextDocument.URI, params.Position, params.NewName) }) } diff --git a/lsp/semantic.go b/lsp/semantic.go index 3e97a12..6449cdf 100644 --- a/lsp/semantic.go +++ b/lsp/semantic.go @@ -10,8 +10,8 @@ import ( ) func (s *Server) semanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) (*protocol.SemanticTokens, error) { - data, err := source.Tokens(ctx, ss, params.TextDocument.URI) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) (*protocol.SemanticTokens, error) { + data, err := source.Tokens(ctx, view, params.TextDocument.URI) if err != nil { return nil, err } diff --git a/lsp/server.go b/lsp/server.go index 1d5ffb0..60f3ecf 100644 --- a/lsp/server.go +++ b/lsp/server.go @@ -374,8 +374,8 @@ func (s *Server) Implementation(ctx context.Context, params *protocol.Implementa } func (s *Server) OnTypeFormatting(ctx context.Context, params *protocol.DocumentOnTypeFormattingParams) (result []protocol.TextEdit, err error) { - return withFile(ctx, s.session, params.TextDocument.URI, func(ss *cache.Snapshot, fh cache.FileHandle) ([]protocol.TextEdit, error) { - return source.OnTypeFormat(ctx, ss, fh, s.formatOptions(ss.View()), params.Position) + return withFile(ctx, s.session, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.TextEdit, error) { + return source.OnTypeFormat(ctx, view, fh, s.formatOptions(view), params.Position) }) } @@ -419,11 +419,7 @@ func (s *Server) Symbols(ctx context.Context, params *protocol.WorkspaceSymbolPa var res []protocol.SymbolInformation for _, view := range views { - ss, release := view.Snapshot() - - syms := source.WorkspaceSymbols(ctx, ss, view.KnownFiles(), params.Query, maxResults-len(res)) - - release() + syms := source.WorkspaceSymbols(ctx, view, view.KnownFiles(), params.Query, maxResults-len(res)) res = append(res, syms...) if len(res) >= maxResults { diff --git a/lsp/snapshot.go b/lsp/snapshot.go index 6ac7b3e..f3f9898 100644 --- a/lsp/snapshot.go +++ b/lsp/snapshot.go @@ -8,10 +8,9 @@ import ( "github.com/karitham/thrift-ls/lsp/cache" ) -// withSnapshot resolves file's view, acquires its snapshot, and runs fn -// while the snapshot is held. Every request handler funnels through this -// helper so the acquire/release discipline lives in one place. -func withSnapshot[T any](session *cache.Session, file uri.URI, fn func(*cache.Snapshot) (T, error)) (T, error) { +// withView resolves file's view and runs fn with it. Every request handler +// funnels through this helper so view routing lives in one place. +func withView[T any](session *cache.Session, file uri.URI, fn func(*cache.View) (T, error)) (T, error) { view, err := session.ViewOf(file) if err != nil { var zero T @@ -19,22 +18,19 @@ func withSnapshot[T any](session *cache.Session, file uri.URI, fn func(*cache.Sn return zero, err } - ss, release := view.Snapshot() - defer release() - - return fn(ss) + return fn(view) } -// withFile is withSnapshot plus the file handle for file. -func withFile[T any](ctx context.Context, session *cache.Session, file uri.URI, fn func(*cache.Snapshot, cache.FileHandle) (T, error)) (T, error) { - return withSnapshot(session, file, func(ss *cache.Snapshot) (T, error) { - fh, err := ss.ReadFile(ctx, file) +// withFile is withView plus the file handle for file. +func withFile[T any](ctx context.Context, session *cache.Session, file uri.URI, fn func(*cache.View, cache.FileHandle) (T, error)) (T, error) { + return withView(session, file, func(view *cache.View) (T, error) { + fh, err := view.ReadFile(ctx, file) if err != nil { var zero T return zero, err } - return fn(ss, fh) + return fn(view, fh) }) } diff --git a/lsp/source/completion_qualified_type_test.go b/lsp/source/completion_qualified_type_test.go index 3c42a73..fa98e94 100644 --- a/lsp/source/completion_qualified_type_test.go +++ b/lsp/source/completion_qualified_type_test.go @@ -47,14 +47,14 @@ const i32 TEMPO = 120` for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte(songs), From: cache.FileChangeTypeDidOpen}, &cache.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(tt.content), From: cache.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, tt.content, tt.marker) - labels, rng, _ := completionLabels(t, ss, "file:///tmp/club.thrift", pos) + labels, rng, _ := completionLabels(t, view, "file:///tmp/club.thrift", pos) assert.Contains(t, labels, "songs.Album", "labels: %v", labels) assert.Contains(t, labels, "songs.Song", "labels: %v", labels) @@ -72,14 +72,14 @@ const i32 TEMPO = 120` func TestCompletionQualifiedTypeFilter(t *testing.T) { content := "include \"songs.thrift\"\nstruct Club {\n\t1: required songs.A\n}" - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte("enum Song {\n\tFUWA_FUWA_TIME = 1\n}\n\nstruct Album {\n\t1: required string title\n}\n"), From: cache.FileChangeTypeDidOpen}, &cache.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, content, "songs.A") - labels, rng, _ := completionLabels(t, ss, "file:///tmp/club.thrift", pos) + labels, rng, _ := completionLabels(t, view, "file:///tmp/club.thrift", pos) assert.Equal(t, []string{"songs.Album"}, labels, "labels: %v", labels) diff --git a/lsp/source/completion_test.go b/lsp/source/completion_test.go index b51d3cf..8bcf9e1 100644 --- a/lsp/source/completion_test.go +++ b/lsp/source/completion_test.go @@ -12,9 +12,9 @@ import ( "github.com/karitham/thrift-ls/options" ) -// buildSnapshot builds a snapshot from file contents with optional include +// buildSnapshot builds a view from file contents with optional include // paths. -func buildSnapshot(t *testing.T, includePaths []string, files ...*cache.FileChange) *cache.Snapshot { +func buildSnapshot(t *testing.T, includePaths []string, files ...*cache.FileChange) *cache.View { t.Helper() c := cache.New() @@ -22,23 +22,27 @@ func buildSnapshot(t *testing.T, includePaths []string, files ...*cache.FileChan _ = fs.Update(t.Context(), files) view := cache.NewView(uri.File("/tmp"), fs, includePaths, options.Patch{}) - return cache.NewSnapshot(view, includePaths) + for _, f := range files { + _, _ = view.Parse(t.Context(), f.URI) + } + + return view } func TestCompletionEndToEnd(t *testing.T) { content := "struct User {\n 1: required i64 id\n}\n\nstruct Profile {\n 1: required Us\n}" - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, ) - fh, err := ss.ReadFile(t.Context(), "file:///tmp/test.thrift") + fh, err := view.ReadFile(t.Context(), "file:///tmp/test.thrift") assert.NoError(t, err) cmp := &CompletionRequest{ Fh: fh, Pos: protocol.Position{Line: 5, Character: 16}, // after "Us" in "1: required Us" } - items, _, _, err := DefaultTokenCompletion.Completion(t.Context(), ss, cmp) + items, _, _, err := DefaultTokenCompletion.Completion(t.Context(), view, cmp) assert.NoError(t, err) labels := make([]string, 0, len(items)) diff --git a/lsp/source/completion_type_slot_test.go b/lsp/source/completion_type_slot_test.go index 559ed01..151b24b 100644 --- a/lsp/source/completion_type_slot_test.go +++ b/lsp/source/completion_type_slot_test.go @@ -39,14 +39,14 @@ struct Album { t.Run(tt.name, func(t *testing.T) { content := "include \"songs.thrift\"\nstruct Club {\n\t" + tt.marker + "\n}" - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte(inc), From: cache.FileChangeTypeDidOpen}, &cache.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, content, tt.marker) - labels, _, _ := completionLabels(t, ss, "file:///tmp/club.thrift", pos) + labels, _, _ := completionLabels(t, view, "file:///tmp/club.thrift", pos) assert.NotContains(t, labels, "FUWA_FUWA_TIME", "enum values must not leak into a type slot") assert.NotContains(t, labels, "TEMPO", "consts must not leak into a type slot") @@ -62,14 +62,14 @@ struct Album { func TestCompletionImportedTypesQualified(t *testing.T) { content := "include \"songs.thrift\"\nstruct Club {\n\t1: required so\n}" - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte("struct Album {\n\t1: required string title\n}\n"), From: cache.FileChangeTypeDidOpen}, &cache.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, content, "required so") - labels, _, _ := completionLabels(t, ss, "file:///tmp/club.thrift", pos) + labels, _, _ := completionLabels(t, view, "file:///tmp/club.thrift", pos) assert.Equal(t, []string{"songs.Album"}, labels, "labels: %v", labels) } diff --git a/lsp/source/cross_project_test.go b/lsp/source/cross_project_test.go index ae0daa9..99048c0 100644 --- a/lsp/source/cross_project_test.go +++ b/lsp/source/cross_project_test.go @@ -26,7 +26,7 @@ func TestDefinitionCrossProjectInclude(t *testing.T) { appFile := uri.File(filepath.Join(appDir, "char.thrift")) appContent := "include \"gundam.thrift\"\n\nstruct Hangar {\n 1: optional list suits,\n}\n\nstruct Garrison {\n 1: optional MobileSuit ace,\n}\n" - ss := cache.BuildSnapshotForTestWithPaths([]string{includeDir}, []*cache.FileChange{ + view := cache.BuildViewForTestWithPaths([]string{includeDir}, []*cache.FileChange{ { URI: gundamFile, Version: 0, @@ -82,7 +82,7 @@ func TestDefinitionCrossProjectInclude(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - locs, err := Definition(t.Context(), ss, appFile, posOf(tt.marker, tt.offset)) + locs, err := Definition(t.Context(), view, appFile, posOf(tt.marker, tt.offset)) assert.NoError(t, err) assert.Len(t, locs, 1, "should find the definition in the included file") assert.Equal(t, gundamFile, locs[0].URI) diff --git a/lsp/source/cross_reference_test.go b/lsp/source/cross_reference_test.go index 898a586..17cdd13 100644 --- a/lsp/source/cross_reference_test.go +++ b/lsp/source/cross_reference_test.go @@ -25,13 +25,13 @@ struct StrikeRouge { 1: required string Name }` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(gundamFile), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainFile), From: cache.FileChangeTypeDidOpen}, }) // Cursor on "Gundam" (the definition name) in federation.gundam.thrift. - locations, err := Reference(t.Context(), ss, "file:///tmp/federation.gundam.thrift", protocol.Position{Line: 0, Character: 7}) + locations, err := Reference(t.Context(), view, "file:///tmp/federation.gundam.thrift", protocol.Position{Line: 0, Character: 7}) assert.NoError(t, err) var uris []string @@ -55,12 +55,12 @@ struct StrikeRouge { 1: required string Name }` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(gundamFile), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainFile), From: cache.FileChangeTypeDidOpen}, }) - locations, err := Reference(t.Context(), ss, "file:///tmp/federation.gundam.thrift", protocol.Position{Line: 0, Character: 7}) + locations, err := Reference(t.Context(), view, "file:///tmp/federation.gundam.thrift", protocol.Position{Line: 0, Character: 7}) assert.NoError(t, err) var uris []string @@ -92,7 +92,7 @@ struct Gundam { 1: required string Model }` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/mobile_suit.zeon.thrift", Version: 0, Content: []byte(zeonFile), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(federationFile), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainFile), From: cache.FileChangeTypeDidOpen}, @@ -100,7 +100,7 @@ struct Gundam { // Cursor on "Zaku" in main.thrift (line 3, character 13: the type of // "1: optional Zaku ride"). - locations, err := Definition(t.Context(), ss, "file:///tmp/main.thrift", protocol.Position{Line: 3, Character: 13}) + locations, err := Definition(t.Context(), view, "file:///tmp/main.thrift", protocol.Position{Line: 3, Character: 13}) assert.NoError(t, err) assert.Len(t, locations, 1) diff --git a/lsp/source/cycle_detect.go b/lsp/source/cycle_detect.go index abcfff3..24a74c7 100644 --- a/lsp/source/cycle_detect.go +++ b/lsp/source/cycle_detect.go @@ -18,19 +18,19 @@ import ( // length are caught, including self-includes. type CycleCheck struct{} -func (c *CycleCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (c *CycleCheck) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { closure := make(map[uri.URI][]Include) for _, file := range changeFiles { - _ = getIncludes(ctx, ss, file, &closure) + _ = getIncludes(ctx, view, file, &closure) } diagnostics := make(DiagnosticResult) for file, includes := range closure { - // Reachability comes from the snapshot's include graph: parsing + // Reachability comes from the view's include graph: parsing // the closure above registered exactly these edges via Register, // so there is no second graph to keep in sync. Dependents is // cycle-safe, so the walk terminates on the cycles it finds. - deps := ss.Dependents(file) + deps := view.Dependents(file) for _, inc := range includes { if !slices.Contains(deps, inc.file) { continue @@ -67,10 +67,10 @@ type Include struct { // getIncludes collects the include closure of file into includesMap: the // include edges of every file reachable from file, parsed through the -// snapshot so the ParsedFiles (and the graph edges Register records) are +// view so the ParsedFiles (and the include edges parsing records) are // shared with the rest of the analysis. -func getIncludes(ctx context.Context, ss *cache.Snapshot, file uri.URI, includesMap *map[uri.URI][]Include) error { - pf, err := ss.Parse(ctx, file) +func getIncludes(ctx context.Context, view *cache.View, file uri.URI, includesMap *map[uri.URI][]Include) error { + pf, err := view.Parse(ctx, file) if err != nil { return err } @@ -84,7 +84,7 @@ func getIncludes(ctx context.Context, ss *cache.Snapshot, file uri.URI, includes } includes := pf.AST().Includes() - resolver := ss.Resolver() + resolver := view.Resolver() for i := range includes { if includes[i].Path == nil { @@ -102,7 +102,7 @@ func getIncludes(ctx context.Context, ss *cache.Snapshot, file uri.URI, includes continue } - _ = getIncludes(ctx, ss, includeURI, includesMap) + _ = getIncludes(ctx, view, includeURI, includesMap) } return nil diff --git a/lsp/source/cycle_detect_test.go b/lsp/source/cycle_detect_test.go index 51021b8..10a465b 100644 --- a/lsp/source/cycle_detect_test.go +++ b/lsp/source/cycle_detect_test.go @@ -15,7 +15,7 @@ import ( "github.com/karitham/thrift-ls/options" ) -func buildSnapshotForTest(t *testing.T, files []*cache.FileChange) *cache.Snapshot { +func buildSnapshotForTest(t *testing.T, files []*cache.FileChange) *cache.View { t.Helper() c := cache.New() @@ -23,9 +23,12 @@ func buildSnapshotForTest(t *testing.T, files []*cache.FileChange) *cache.Snapsh _ = fs.Update(t.Context(), files) view := cache.NewView("file:///tmp", fs, nil, options.Patch{}) - ss := cache.NewSnapshot(view, nil) - return ss + for _, f := range files { + _, _ = view.Parse(t.Context(), f.URI) + } + + return view } // cyclePair identifies one reported cycle include: the file containing the @@ -86,9 +89,9 @@ func runCycleCheck(t *testing.T, files map[string]string, root string) []cyclePa }) } - ss := buildSnapshotForTest(t, changes) + view := buildSnapshotForTest(t, changes) - res, err := (&CycleCheck{}).Diagnostic(t.Context(), ss, []uri.URI{uri.URI("file:///tmp/" + root)}) + res, err := (&CycleCheck{}).Diagnostic(t.Context(), view, []uri.URI{uri.URI("file:///tmp/" + root)}) require.NoError(t, err) for file, diags := range res { @@ -230,7 +233,7 @@ include "./test/address.thrift"` file2 := `include "../user.thrift"` file3 := `include "../user.thrift"` - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -254,7 +257,7 @@ include "./test/address.thrift"` // Expected includes, parsed through the same snapshot so the ParsedFile // pointers match the ones getIncludes stores. pfFor := func(uriStr string) *cache.ParsedFile { - pf, err := ss.Parse(t.Context(), uri.URI(uriStr)) + pf, err := view.Parse(t.Context(), uri.URI(uriStr)) require.NoError(t, err) return pf @@ -278,7 +281,7 @@ include "./test/address.thrift"` includeMap := make(map[uri.URI][]Include) - err := getIncludes(t.Context(), ss, "file:///tmp/user.thrift", &includeMap) + err := getIncludes(t.Context(), view, "file:///tmp/user.thrift", &includeMap) require.NoError(t, err) assert.Equal(t, expectIncludeMap, includeMap) diff --git a/lsp/source/definition.go b/lsp/source/definition.go index bd00524..e5e7cde 100644 --- a/lsp/source/definition.go +++ b/lsp/source/definition.go @@ -12,21 +12,21 @@ import ( // Definition returns the locations of the definition under the cursor: // a type reference, a constant value identifier, or a service reference. -func Definition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { +func Definition(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) - pf, target, err := resolveTarget(ctx, ss, file, pos) + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return res, err } switch target.kind { case TargetTypeName: - return typeNameDefinition(ctx, NewIndex(ss), pf, target) + return typeNameDefinition(ctx, NewIndex(view), pf, target) case TargetConstValue: - return constValueDefinition(ctx, NewIndex(ss), pf, target) + return constValueDefinition(ctx, NewIndex(view), pf, target) case TargetService: - return serviceDefinition(ctx, NewIndex(ss), pf, target) + return serviceDefinition(ctx, NewIndex(view), pf, target) } return res, err @@ -40,7 +40,7 @@ func typeNameDefinition(ctx context.Context, ix *Index, pf *cache.ParsedFile, ta return nil, err } - loc, err := jumpInFile(ctx, ix.ss, def.File, def.Name) + loc, err := jumpInFile(ctx, ix.view, def.File, def.Name) if err != nil { return nil, err } @@ -54,7 +54,7 @@ func constValueDefinition(ctx context.Context, ix *Index, pf *cache.ParsedFile, return nil, err } - loc, err := jumpInFile(ctx, ix.ss, def.File, def.Name) + loc, err := jumpInFile(ctx, ix.view, def.File, def.Name) if err != nil { return nil, err } @@ -68,7 +68,7 @@ func serviceDefinition(ctx context.Context, ix *Index, pf *cache.ParsedFile, tar return nil, err } - loc, err := jumpInFile(ctx, ix.ss, def.File, def.Name) + loc, err := jumpInFile(ctx, ix.view, def.File, def.Name) if err != nil { return nil, err } diff --git a/lsp/source/definition_list_test.go b/lsp/source/definition_list_test.go index a0ce25a..dd0ae80 100644 --- a/lsp/source/definition_list_test.go +++ b/lsp/source/definition_list_test.go @@ -23,7 +23,7 @@ func TestDefinition_EnumValueInConstList(t *testing.T) { const list my_list = [MyEnum.Value1, MyEnum.Value2]` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/test.thrift", Version: 0, @@ -34,7 +34,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View file uri.URI pos protocol.Position } @@ -49,7 +49,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` name: "enum value in const list - first element", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/test.thrift", pos: protocol.Position{ Line: 6, // Line with "const list my_list = [MyEnum.Value1, MyEnum.Value2]" @@ -77,7 +77,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` name: "enum value in const list - second element", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/test.thrift", pos: protocol.Position{ Line: 6, @@ -105,7 +105,7 @@ const list my_list = [MyEnum.Value1, MyEnum.Value2]` for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Definition(tt.args.ctx, tt.args.ss, tt.args.file, tt.args.pos) + got, err := Definition(tt.args.ctx, tt.args.view, tt.args.file, tt.args.pos) tt.assertion(t, err) assert.Equal(t, tt.want, got) }) diff --git a/lsp/source/definition_test.go b/lsp/source/definition_test.go index 984d54e..aed3fbc 100644 --- a/lsp/source/definition_test.go +++ b/lsp/source/definition_test.go @@ -58,7 +58,7 @@ struct Person { 2: required user.Test field2, }` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -87,7 +87,7 @@ struct Person { type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View file uri.URI pos protocol.Position } @@ -102,7 +102,7 @@ struct Person { name: "case struct", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -130,7 +130,7 @@ struct Person { name: "case union", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -158,7 +158,7 @@ struct Person { name: "case enum", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -186,7 +186,7 @@ struct Person { name: "case exceptions", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -214,7 +214,7 @@ struct Person { name: "case typedef", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -242,7 +242,7 @@ struct Person { name: "case enumvalue", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -270,7 +270,7 @@ struct Person { name: "case const", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -298,7 +298,7 @@ struct Person { name: "case include 1", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/app.thrift", pos: protocol.Position{ Line: 4, @@ -326,7 +326,7 @@ struct Person { name: "case include 2", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/app.thrift", pos: protocol.Position{ Line: 5, @@ -353,7 +353,7 @@ struct Person { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Definition(tt.args.ctx, tt.args.ss, tt.args.file, tt.args.pos) + got, err := Definition(tt.args.ctx, tt.args.view, tt.args.file, tt.args.pos) tt.assertion(t, err) assert.Equal(t, tt.want, got) }) diff --git a/lsp/source/diagnostic.go b/lsp/source/diagnostic.go index 882da41..0bee6ff 100644 --- a/lsp/source/diagnostic.go +++ b/lsp/source/diagnostic.go @@ -28,7 +28,7 @@ func init() { } type Checker interface { - Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) + Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) Name() string } @@ -38,7 +38,7 @@ func NewDiagnostic() Checker { return &Diagnostic{} } -func (d *Diagnostic) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (d *Diagnostic) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) var errs []error @@ -46,7 +46,7 @@ func (d *Diagnostic) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeF for _, impl := range registry { slog.Debug("diagnostic called", "impl", impl.Name()) - diagRes, err := impl.Diagnostic(ctx, ss, changeFiles) + diagRes, err := impl.Diagnostic(ctx, view, changeFiles) if err != nil { errs = append(errs, err) } diff --git a/lsp/source/document.go b/lsp/source/document.go index dcb4853..ac502fe 100644 --- a/lsp/source/document.go +++ b/lsp/source/document.go @@ -11,10 +11,10 @@ import ( ) // DocumentSymbols returns the document symbols of a file, in source order. -func DocumentSymbols(ctx context.Context, ss *cache.Snapshot, file uri.URI) []*protocol.DocumentSymbol { +func DocumentSymbols(ctx context.Context, view *cache.View, file uri.URI) []*protocol.DocumentSymbol { res := make([]*protocol.DocumentSymbol, 0) - pf, err := ss.Parse(ctx, file) + pf, err := view.Parse(ctx, file) if err != nil { return res } diff --git a/lsp/source/duplicate_check.go b/lsp/source/duplicate_check.go index a7ff72c..fc901ee 100644 --- a/lsp/source/duplicate_check.go +++ b/lsp/source/duplicate_check.go @@ -26,11 +26,11 @@ func (c *DuplicateCheck) Name() string { return "DuplicateCheck" } -func (c *DuplicateCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (c *DuplicateCheck) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) for _, file := range changeFiles { - items, err := c.diagnostic(ctx, ss, file) + items, err := c.diagnostic(ctx, view, file) if err != nil { return nil, err } @@ -41,8 +41,8 @@ func (c *DuplicateCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, cha return res, nil } -func (c *DuplicateCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, file uri.URI) ([]protocol.Diagnostic, error) { - pf, err := ss.Parse(ctx, file) +func (c *DuplicateCheck) diagnostic(ctx context.Context, view *cache.View, file uri.URI) ([]protocol.Diagnostic, error) { + pf, err := view.Parse(ctx, file) if err != nil { return nil, err } diff --git a/lsp/source/duplicate_check_test.go b/lsp/source/duplicate_check_test.go index cdd1219..213debc 100644 --- a/lsp/source/duplicate_check_test.go +++ b/lsp/source/duplicate_check_test.go @@ -350,7 +350,7 @@ enum User {} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -359,7 +359,7 @@ enum User {} }, }) - got, err := (&DuplicateCheck{}).Diagnostic(t.Context(), ss, []uri.URI{"file:///tmp/user.thrift"}) + got, err := (&DuplicateCheck{}).Diagnostic(t.Context(), view, []uri.URI{"file:///tmp/user.thrift"}) assert.NoError(t, err) assert.Equal(t, DiagnosticResult{"file:///tmp/user.thrift": tt.want}, got) diff --git a/lsp/source/enum_value_action.go b/lsp/source/enum_value_action.go index a58070b..25cea96 100644 --- a/lsp/source/enum_value_action.go +++ b/lsp/source/enum_value_action.go @@ -19,8 +19,8 @@ import ( // It returns nil when the selection is outside every enum, the enum is // already fully explicit, the implicit values cannot be computed (an // unparseable explicit constant), or the document has parse errors. -func MakeEnumValuesExplicitAction(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, rng protocol.Range) (*protocol.CodeAction, error) { - pf, err := ss.Parse(ctx, fh.URI()) +func MakeEnumValuesExplicitAction(ctx context.Context, view *cache.View, fh cache.FileHandle, rng protocol.Range) (*protocol.CodeAction, error) { + pf, err := view.Parse(ctx, fh.URI()) if err != nil { return nil, err } diff --git a/lsp/source/enum_value_action_test.go b/lsp/source/enum_value_action_test.go index 4454e86..07abae5 100644 --- a/lsp/source/enum_value_action_test.go +++ b/lsp/source/enum_value_action_test.go @@ -86,7 +86,7 @@ enum B { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -95,10 +95,10 @@ enum B { }, }) - fh, err := ss.ReadFile(t.Context(), "file:///tmp/user.thrift") + fh, err := view.ReadFile(t.Context(), "file:///tmp/user.thrift") require.NoError(t, err) - act, err := MakeEnumValuesExplicitAction(t.Context(), ss, fh, tt.rng) + act, err := MakeEnumValuesExplicitAction(t.Context(), view, fh, tt.rng) require.NoError(t, err) if tt.want == "" { diff --git a/lsp/source/enum_value_check.go b/lsp/source/enum_value_check.go index a6eb972..4fc3ec1 100644 --- a/lsp/source/enum_value_check.go +++ b/lsp/source/enum_value_check.go @@ -22,11 +22,11 @@ type EnumValueCheck struct{} // one greater than the preceding member's value otherwise. Their on-wire // value therefore follows their position; inserting, removing, or // reordering members silently changes serialized data. -func (c *EnumValueCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (c *EnumValueCheck) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) for _, file := range changeFiles { - items, err := c.diagnostic(ctx, ss, file) + items, err := c.diagnostic(ctx, view, file) if err != nil { return nil, err } @@ -41,8 +41,8 @@ func (c *EnumValueCheck) Name() string { return "EnumValueCheck" } -func (c *EnumValueCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, file uri.URI) ([]protocol.Diagnostic, error) { - pf, err := ss.Parse(ctx, file) +func (c *EnumValueCheck) diagnostic(ctx context.Context, view *cache.View, file uri.URI) ([]protocol.Diagnostic, error) { + pf, err := view.Parse(ctx, file) if err != nil { return nil, err } diff --git a/lsp/source/enum_value_check_test.go b/lsp/source/enum_value_check_test.go index 681ebfd..7024b86 100644 --- a/lsp/source/enum_value_check_test.go +++ b/lsp/source/enum_value_check_test.go @@ -92,7 +92,7 @@ func Test_EnumValueCheck_Diagnostic(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -101,7 +101,7 @@ func Test_EnumValueCheck_Diagnostic(t *testing.T) { }, }) - got, err := (&EnumValueCheck{}).Diagnostic(t.Context(), ss, []uri.URI{"file:///tmp/user.thrift"}) + got, err := (&EnumValueCheck{}).Diagnostic(t.Context(), view, []uri.URI{"file:///tmp/user.thrift"}) assert.NoError(t, err) assert.Equal(t, DiagnosticResult{"file:///tmp/user.thrift": tt.want}, got) diff --git a/lsp/source/field_qualifier_action.go b/lsp/source/field_qualifier_action.go index 226b359..5244b3e 100644 --- a/lsp/source/field_qualifier_action.go +++ b/lsp/source/field_qualifier_action.go @@ -25,8 +25,8 @@ type pickedFieldAction struct { code protocol.CodeAction } -func MakeFieldQualifierAction(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, rng protocol.Range) ([]protocol.CodeAction, error) { - pf, err := ss.Parse(ctx, fh.URI()) +func MakeFieldQualifierAction(ctx context.Context, view *cache.View, fh cache.FileHandle, rng protocol.Range) ([]protocol.CodeAction, error) { + pf, err := view.Parse(ctx, fh.URI()) if err != nil { return nil, err } diff --git a/lsp/source/fieldid_check.go b/lsp/source/fieldid_check.go index 7237e44..42a4319 100644 --- a/lsp/source/fieldid_check.go +++ b/lsp/source/fieldid_check.go @@ -17,11 +17,11 @@ type FieldIDCheck struct{} // FieldIDCheck checks struct, union, exception, function parameter, and // throws field ids: they must be unique positive integers in [1, 32767]. -func (c *FieldIDCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (c *FieldIDCheck) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) for _, file := range changeFiles { - items, err := c.diagnostic(ctx, ss, file) + items, err := c.diagnostic(ctx, view, file) if err != nil { return nil, err } @@ -36,8 +36,8 @@ func (c *FieldIDCheck) Name() string { return "FieldIDCheck" } -func (c *FieldIDCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, file uri.URI) ([]protocol.Diagnostic, error) { - pf, err := ss.Parse(ctx, file) +func (c *FieldIDCheck) diagnostic(ctx context.Context, view *cache.View, file uri.URI) ([]protocol.Diagnostic, error) { + pf, err := view.Parse(ctx, file) if err != nil { return nil, err } diff --git a/lsp/source/fieldid_check_test.go b/lsp/source/fieldid_check_test.go index 8ba91d6..b36222c 100644 --- a/lsp/source/fieldid_check_test.go +++ b/lsp/source/fieldid_check_test.go @@ -40,7 +40,7 @@ service Demo { } ` - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -51,7 +51,7 @@ service Demo { type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View changeFiles []uri.URI } @@ -66,8 +66,8 @@ service Demo { name: "case1", c: &FieldIDCheck{}, args: args{ - ctx: t.Context(), - ss: ss, + ctx: t.Context(), + view: view, changeFiles: []uri.URI{ "file:///tmp/user.thrift", }, @@ -411,7 +411,7 @@ service Demo { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &FieldIDCheck{} - got, err := c.Diagnostic(tt.args.ctx, tt.args.ss, tt.args.changeFiles) + got, err := c.Diagnostic(tt.args.ctx, tt.args.view, tt.args.changeFiles) for key := range got { sort.SliceStable(got[key], func(i, j int) bool { diff --git a/lsp/source/folding.go b/lsp/source/folding.go index 16e610c..a831f56 100644 --- a/lsp/source/folding.go +++ b/lsp/source/folding.go @@ -1,6 +1,6 @@ // Package folding computes document folding ranges: braced bodies // (structs, enums, services), const list and map values, annotations, and -// comment blocks. Pure over the snapshot: parsing and file I/O happen in +// comment blocks. Pure over the view: parsing and file I/O happen in // the caller. package source @@ -18,8 +18,8 @@ import ( // Ranges returns the folding ranges of a file, in source order. Degenerate // single-line ranges are omitted. -func Ranges(ctx context.Context, ss *cache.Snapshot, file uri.URI) []protocol.FoldingRange { - pf, err := ss.Parse(ctx, file) +func Ranges(ctx context.Context, view *cache.View, file uri.URI) []protocol.FoldingRange { + pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil } diff --git a/lsp/source/folding_test.go b/lsp/source/folding_test.go index b48442d..5817889 100644 --- a/lsp/source/folding_test.go +++ b/lsp/source/folding_test.go @@ -37,10 +37,7 @@ func foldingRanges(t *testing.T, src string) []protocol.FoldingRange { From: cache.FileChangeTypeInitialize, }}) - ss, release := view.Snapshot() - defer release() - - return Ranges(t.Context(), ss, file) + return Ranges(t.Context(), view, file) } func TestFoldingRanges(t *testing.T) { diff --git a/lsp/source/format.go b/lsp/source/format.go index f24c148..30b9bc4 100644 --- a/lsp/source/format.go +++ b/lsp/source/format.go @@ -19,8 +19,8 @@ import ( var ErrNotParseable = errors.New("document does not parse") // Format returns the whole-document formatting of fh's content. -func Format(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, opts formatter.Options) (string, error) { - pf, err := ss.Parse(ctx, fh.URI()) +func Format(ctx context.Context, view *cache.View, fh cache.FileHandle, opts formatter.Options) (string, error) { + pf, err := view.Parse(ctx, fh.URI()) if err != nil { return "", err } @@ -35,13 +35,13 @@ func Format(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, opts f // FormatDocument returns the single text edit replacing the whole document // with its formatted content. It returns nil when the document is already // formatted. -func FormatDocument(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, opts formatter.Options) (*protocol.TextEdit, error) { +func FormatDocument(ctx context.Context, view *cache.View, fh cache.FileHandle, opts formatter.Options) (*protocol.TextEdit, error) { content, err := fh.Content() if err != nil { return nil, err } - formatted, err := Format(ctx, ss, fh, opts) + formatted, err := Format(ctx, view, fh, opts) if errors.Is(err, ErrNotParseable) { return nil, nil // the Parse checker reports the errors } @@ -76,13 +76,13 @@ func FormatDocument(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle // are preserved exactly by the formatter, so the blocks align one-to-one; // every edit is bounded by blank lines or file edges, and any subset // splices safely. Only the edits overlapping the selection are returned. -func FormatRange(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, opts formatter.Options, rng protocol.Range) ([]protocol.TextEdit, error) { +func FormatRange(ctx context.Context, view *cache.View, fh cache.FileHandle, opts formatter.Options, rng protocol.Range) ([]protocol.TextEdit, error) { content, err := fh.Content() if err != nil { return nil, err } - formatted, err := Format(ctx, ss, fh, opts) + formatted, err := Format(ctx, view, fh, opts) if errors.Is(err, ErrNotParseable) { return nil, nil // the Parse checker reports the errors } @@ -278,8 +278,8 @@ func lineEnd(content []byte, offset int) int { // typed: the whole struct/union/exception/enum/service block reflows. A // document that does not parse, or a position outside any construct, // formats nothing. -func OnTypeFormat(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, opts formatter.Options, pos protocol.Position) ([]protocol.TextEdit, error) { - pf, err := ss.Parse(ctx, fh.URI()) +func OnTypeFormat(ctx context.Context, view *cache.View, fh cache.FileHandle, opts formatter.Options, pos protocol.Position) ([]protocol.TextEdit, error) { + pf, err := view.Parse(ctx, fh.URI()) if err != nil || pf.AST() == nil { return nil, nil } @@ -289,7 +289,7 @@ func OnTypeFormat(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, return nil, nil } - return FormatRange(ctx, ss, fh, opts, *rng) + return FormatRange(ctx, view, fh, opts, *rng) } // enclosingConstruct returns the range of the top-level construct diff --git a/lsp/source/highlight_test.go b/lsp/source/highlight_test.go index af8f270..1b87dc2 100644 --- a/lsp/source/highlight_test.go +++ b/lsp/source/highlight_test.go @@ -67,9 +67,9 @@ struct StrikeRouge { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := cache.BuildSnapshotForTest(tt.files) + view := cache.BuildViewForTest(tt.files) - highlights, err := Highlight(t.Context(), ss, tt.files[0].URI, tt.pos) + highlights, err := Highlight(t.Context(), view, tt.files[0].URI, tt.pos) require.NoError(t, err) lines := make([]uint32, len(highlights)) @@ -86,11 +86,11 @@ struct StrikeRouge { // TestHighlightUnresolvableType pins the minimal result for an identifier // without references: only the cursor word itself is highlighted. func TestHighlightUnresolvableType(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte("struct Gundam {\n\t1: required UnknownType pack\n}"), From: cache.FileChangeTypeDidOpen}, }) - highlights, err := Highlight(t.Context(), ss, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 20}) + highlights, err := Highlight(t.Context(), view, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 20}) require.NoError(t, err) require.Len(t, highlights, 1) assert.Equal(t, uint32(1), highlights[0].Range.Start.Line) diff --git a/lsp/source/hover.go b/lsp/source/hover.go index 5c55dc3..9730f0c 100644 --- a/lsp/source/hover.go +++ b/lsp/source/hover.go @@ -13,13 +13,13 @@ import ( // Hover returns the formatted definition under the cursor: a type // reference, a constant value identifier, or a service reference. -func Hover(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res string, err error) { - pf, target, err := resolveTarget(ctx, ss, file, pos) +func Hover(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) (res string, err error) { + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return res, err } - ix := NewIndex(ss) + ix := NewIndex(view) switch target.kind { case TargetTypeName: diff --git a/lsp/source/hover_test.go b/lsp/source/hover_test.go index 2f249d9..47bf2fa 100644 --- a/lsp/source/hover_test.go +++ b/lsp/source/hover_test.go @@ -31,7 +31,7 @@ service Svc extends Base { } const Color defaultColor = GREEN` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), From: cache.FileChangeTypeDidOpen}, }) @@ -76,7 +76,7 @@ const Color defaultColor = GREEN` } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Hover(t.Context(), ss, uri.URI(tt.file), posOf(tt.content, tt.marker, tt.offset)) + got, err := Hover(t.Context(), view, uri.URI(tt.file), posOf(tt.content, tt.marker, tt.offset)) if err != nil { t.Fatalf("Hover: %v", err) } @@ -89,11 +89,11 @@ const Color defaultColor = GREEN` } func TestHoverUnresolvable(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/test.thrift", Version: 0, Content: []byte("struct S {\n 1: Missing x\n}"), From: cache.FileChangeTypeDidOpen}, }) - got, err := Hover(t.Context(), ss, "file:///tmp/test.thrift", protocol.Position{Line: 1, Character: 8}) + got, err := Hover(t.Context(), view, "file:///tmp/test.thrift", protocol.Position{Line: 1, Character: 8}) if err != nil { t.Fatalf("Hover: %v", err) } diff --git a/lsp/source/include_action.go b/lsp/source/include_action.go index 3238eb3..b6da08e 100644 --- a/lsp/source/include_action.go +++ b/lsp/source/include_action.go @@ -16,8 +16,8 @@ import ( // MakeRemoveUnusedIncludeAction returns the quickfix that deletes the // include line for an "unused include" diagnostic on the selection. It // returns nil when no such diagnostic overlaps the selection. -func MakeRemoveUnusedIncludeAction(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, rng protocol.Range, diags []protocol.Diagnostic) (*protocol.CodeAction, error) { - pf, err := ss.Parse(ctx, fh.URI()) +func MakeRemoveUnusedIncludeAction(ctx context.Context, view *cache.View, fh cache.FileHandle, rng protocol.Range, diags []protocol.Diagnostic) (*protocol.CodeAction, error) { + pf, err := view.Parse(ctx, fh.URI()) if err != nil { return nil, err } @@ -92,8 +92,8 @@ func unusedIncludeAt(pf *cache.ParsedFile, rng protocol.Range, diags []protocol. // workspace folder, so the fix works across the whole project. It returns // nil when the selection has no such diagnostic, the referenced type is // not found anywhere, or the current file cannot be edited (parse errors). -func MakeAddMissingIncludeAction(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, rng protocol.Range, diags []protocol.Diagnostic) (*protocol.CodeAction, error) { - pf, err := ss.Parse(ctx, fh.URI()) +func MakeAddMissingIncludeAction(ctx context.Context, view *cache.View, fh cache.FileHandle, rng protocol.Range, diags []protocol.Diagnostic) (*protocol.CodeAction, error) { + pf, err := view.Parse(ctx, fh.URI()) if err != nil { return nil, err } @@ -102,12 +102,12 @@ func MakeAddMissingIncludeAction(ctx context.Context, ss *cache.Snapshot, fh cac return nil, nil } - name := missingTypeAt(ctx, ss, fh, rng, diags) + name := missingTypeAt(ctx, view, fh, rng, diags) if name == "" { return nil, nil } - def, err := NewIndex(ss).FindInWorkspace(ctx, name) + def, err := NewIndex(view).FindInWorkspace(ctx, name) if err != nil || def == nil || def.File == fh.URI() { return nil, nil } @@ -146,7 +146,7 @@ func MakeAddMissingIncludeAction(ctx context.Context, ss *cache.Snapshot, fh cac // missingTypeAt returns the type name of a "field type doesn't exist" // diagnostic on the selection, or "". -func missingTypeAt(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, rng protocol.Range, diags []protocol.Diagnostic) string { +func missingTypeAt(ctx context.Context, view *cache.View, fh cache.FileHandle, rng protocol.Range, diags []protocol.Diagnostic) string { var diagnosticRange protocol.Range found := false @@ -163,7 +163,7 @@ func missingTypeAt(ctx context.Context, ss *cache.Snapshot, fh cache.FileHandle, return "" } - _, target, err := resolveTarget(ctx, ss, fh.URI(), diagnosticRange.Start) + _, target, err := resolveTarget(ctx, view, fh.URI(), diagnosticRange.Start) if err != nil { return "" } diff --git a/lsp/source/include_action_test.go b/lsp/source/include_action_test.go index 9c2b8cf..cdacdaa 100644 --- a/lsp/source/include_action_test.go +++ b/lsp/source/include_action_test.go @@ -17,7 +17,7 @@ import ( // buildFolderSnapshotForTest builds a snapshot whose view root is folder, // with the given files opened in the overlay. -func buildFolderSnapshotForTest(t *testing.T, folder string, files []*cache.FileChange) *cache.Snapshot { +func buildFolderSnapshotForTest(t *testing.T, folder string, files []*cache.FileChange) *cache.View { t.Helper() c := cache.New() @@ -26,7 +26,11 @@ func buildFolderSnapshotForTest(t *testing.T, folder string, files []*cache.File view := cache.NewView(uri.File(folder), fs, nil, options.Patch{}) - return cache.NewSnapshot(view, nil) + for _, f := range files { + _, _ = view.Parse(t.Context(), f.URI) + } + + return view } // writeThrift writes content to a .thrift file under folder. @@ -43,7 +47,7 @@ func Test_MakeRemoveUnusedIncludeAction(t *testing.T) { folder := t.TempDir() filePath := writeThrift(t, folder, "user.thrift", "include \"shared.thrift\"\nstruct S { 1: i32 a }\n") - ss := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ { URI: uri.File(filePath), Version: 0, @@ -52,15 +56,15 @@ func Test_MakeRemoveUnusedIncludeAction(t *testing.T) { }, }) - fh, err := ss.ReadFile(t.Context(), uri.File(filePath)) + fh, err := view.ReadFile(t.Context(), uri.File(filePath)) require.NoError(t, err) // The diagnostic the check produces, as the server would pass it. - diags, err := (&UnusedIncludeCheck{}).diagnostic(t.Context(), ss, uri.File(filePath)) + diags, err := (&UnusedIncludeCheck{}).diagnostic(t.Context(), view, uri.File(filePath)) require.NoError(t, err) require.Len(t, diags, 1) - act, err := MakeRemoveUnusedIncludeAction(t.Context(), ss, fh, diags[0].Range, diags) + act, err := MakeRemoveUnusedIncludeAction(t.Context(), view, fh, diags[0].Range, diags) require.NoError(t, err) require.NotNil(t, act) assert.Equal(t, protocol.CodeActionKindQuickFix, *act.Kind) @@ -75,7 +79,7 @@ func Test_MakeRemoveUnusedIncludeAction_NoDiagnostic(t *testing.T) { folder := t.TempDir() filePath := writeThrift(t, folder, "user.thrift", "include \"shared.thrift\"\nstruct S { 1: shared.User u }\n") - ss := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ { URI: uri.File(filePath), Version: 0, @@ -84,10 +88,10 @@ func Test_MakeRemoveUnusedIncludeAction_NoDiagnostic(t *testing.T) { }, }) - fh, err := ss.ReadFile(t.Context(), uri.File(filePath)) + fh, err := view.ReadFile(t.Context(), uri.File(filePath)) require.NoError(t, err) - act, err := MakeRemoveUnusedIncludeAction(t.Context(), ss, fh, pointRange(0, 0), nil) + act, err := MakeRemoveUnusedIncludeAction(t.Context(), view, fh, pointRange(0, 0), nil) require.NoError(t, err) assert.Nil(t, act) } @@ -97,7 +101,7 @@ func Test_MakeAddMissingIncludeAction(t *testing.T) { _ = writeThrift(t, folder, "shared.thrift", "struct User {\n 1: i32 id,\n}\n") filePath := writeThrift(t, folder, "user.thrift", "struct S {\n 1: User u,\n}\n") - ss := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ { URI: uri.File(filePath), Version: 0, @@ -106,7 +110,7 @@ func Test_MakeAddMissingIncludeAction(t *testing.T) { }, }) - fh, err := ss.ReadFile(t.Context(), uri.File(filePath)) + fh, err := view.ReadFile(t.Context(), uri.File(filePath)) require.NoError(t, err) // The semantic diagnostic the server would pass, at the type position. @@ -116,7 +120,7 @@ func Test_MakeAddMissingIncludeAction(t *testing.T) { Message: protocol.String("field type doesn't exist"), } - act, err := MakeAddMissingIncludeAction(t.Context(), ss, fh, diag.Range, []protocol.Diagnostic{diag}) + act, err := MakeAddMissingIncludeAction(t.Context(), view, fh, diag.Range, []protocol.Diagnostic{diag}) require.NoError(t, err) require.NotNil(t, act) assert.Equal(t, protocol.CodeActionKindQuickFix, *act.Kind) @@ -133,7 +137,7 @@ func Test_MakeAddMissingIncludeAction_InsertAfterExistingIncludes(t *testing.T) _ = writeThrift(t, folder, "shared.thrift", "struct User {\n 1: i32 id,\n}\n") filePath := writeThrift(t, folder, "user.thrift", "include \"base.thrift\"\n\nstruct S {\n 1: User u,\n}\n") - ss := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ { URI: uri.File(filePath), Version: 0, @@ -142,7 +146,7 @@ func Test_MakeAddMissingIncludeAction_InsertAfterExistingIncludes(t *testing.T) }, }) - fh, err := ss.ReadFile(t.Context(), uri.File(filePath)) + fh, err := view.ReadFile(t.Context(), uri.File(filePath)) require.NoError(t, err) diag := protocol.Diagnostic{ @@ -151,7 +155,7 @@ func Test_MakeAddMissingIncludeAction_InsertAfterExistingIncludes(t *testing.T) Message: protocol.String("field type doesn't exist"), } - act, err := MakeAddMissingIncludeAction(t.Context(), ss, fh, diag.Range, []protocol.Diagnostic{diag}) + act, err := MakeAddMissingIncludeAction(t.Context(), view, fh, diag.Range, []protocol.Diagnostic{diag}) require.NoError(t, err) require.NotNil(t, act) @@ -165,7 +169,7 @@ func Test_MakeAddMissingIncludeAction_TypeNotFound(t *testing.T) { folder := t.TempDir() filePath := writeThrift(t, folder, "user.thrift", "struct S {\n 1: Ghost u,\n}\n") - ss := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ { URI: uri.File(filePath), Version: 0, @@ -174,7 +178,7 @@ func Test_MakeAddMissingIncludeAction_TypeNotFound(t *testing.T) { }, }) - fh, err := ss.ReadFile(t.Context(), uri.File(filePath)) + fh, err := view.ReadFile(t.Context(), uri.File(filePath)) require.NoError(t, err) diag := protocol.Diagnostic{ @@ -183,7 +187,7 @@ func Test_MakeAddMissingIncludeAction_TypeNotFound(t *testing.T) { Message: protocol.String("field type doesn't exist"), } - act, err := MakeAddMissingIncludeAction(t.Context(), ss, fh, diag.Range, []protocol.Diagnostic{diag}) + act, err := MakeAddMissingIncludeAction(t.Context(), view, fh, diag.Range, []protocol.Diagnostic{diag}) require.NoError(t, err) assert.Nil(t, act) } diff --git a/lsp/source/index.go b/lsp/source/index.go index fac96c6..95dad74 100644 --- a/lsp/source/index.go +++ b/lsp/source/index.go @@ -14,7 +14,7 @@ import ( "github.com/karitham/thrift-ls/syntax" ) -// Index answers cross-file semantic queries over one snapshot: definition +// Index answers cross-file semantic queries over one view: definition // resolution and reference search. It composes per-file // cache.FileIndexes over the include graph. // @@ -22,14 +22,14 @@ import ( // are memoized per (file, name), so a request resolving the same name in // the same file repeatedly (references, diagnostics) resolves it once. type Index struct { - ss *cache.Snapshot + view *cache.View resolved map[resolveKey]*Resolved } -// NewIndex returns an Index for the snapshot. -func NewIndex(ss *cache.Snapshot) *Index { - return &Index{ss: ss} +// NewIndex returns an Index over the view's store. +func NewIndex(view *cache.View) *Index { + return &Index{view: view} } // resolveKey identifies one resolution: the referencing file, the name as @@ -69,8 +69,8 @@ func (x *Index) memoize(file uri.URI, name string, kind resolveKind, def *Resolv // parseDefinitionFile parses the definition file, tolerating parse errors // in the target file (the definitions may still be found in the partial // AST). It returns the parsed file so callers can use its indexes. -func parseDefinitionFile(ctx context.Context, ss *cache.Snapshot, file uri.URI) (*cache.ParsedFile, error) { - pf, err := ss.Parse(ctx, file) +func parseDefinitionFile(ctx context.Context, view *cache.View, file uri.URI) (*cache.ParsedFile, error) { + pf, err := view.Parse(ctx, file) if err != nil { return nil, err } @@ -129,8 +129,8 @@ func (x *Index) ResolveType(ctx context.Context, from *cache.ParsedFile, ft *syn // resolveType resolves a non-basic type name in from, without memoization. func (x *Index) resolveType(ctx context.Context, from *cache.ParsedFile, name string) (*Resolved, error) { _, identifier := parseIdent(from.URI(), from.AST().Includes(), name) - for _, astFile := range definitionFiles(ctx, x.ss, from.URI(), from.AST(), name) { - dst, err := parseDefinitionFile(ctx, x.ss, astFile) + for _, astFile := range definitionFiles(ctx, x.view, from.URI(), from.AST(), name) { + dst, err := parseDefinitionFile(ctx, x.view, astFile) if err != nil { return nil, err } @@ -179,8 +179,8 @@ func (x *Index) resolveValue(ctx context.Context, from *cache.ParsedFile, text s _, identifier := parseIdent(from.URI(), from.AST().Includes(), text) identifier = bareName(identifier) - for _, astFile := range definitionFiles(ctx, x.ss, from.URI(), from.AST(), text) { - dst, err := parseDefinitionFile(ctx, x.ss, astFile) + for _, astFile := range definitionFiles(ctx, x.view, from.URI(), from.AST(), text) { + dst, err := parseDefinitionFile(ctx, x.view, astFile) if err != nil { return nil, err } @@ -222,8 +222,8 @@ func (x *Index) ResolveService(ctx context.Context, from *cache.ParsedFile, iden // memoization. func (x *Index) resolveService(ctx context.Context, from *cache.ParsedFile, name string) (*Resolved, error) { _, identifier := parseIdent(from.URI(), from.AST().Includes(), name) - for _, astFile := range definitionFiles(ctx, x.ss, from.URI(), from.AST(), name) { - dst, err := parseDefinitionFile(ctx, x.ss, astFile) + for _, astFile := range definitionFiles(ctx, x.view, from.URI(), from.AST(), name) { + dst, err := parseDefinitionFile(ctx, x.view, astFile) if err != nil { return nil, err } @@ -266,7 +266,7 @@ func (x *Index) References(ctx context.Context, file uri.URI, name string, kinds seen[f] = true - pf, err := x.ss.Parse(ctx, f) + pf, err := x.view.Parse(ctx, f) if err != nil || pf.AST() == nil { continue } @@ -290,7 +290,7 @@ func (x *Index) ReferencesTo(ctx context.Context, def *Resolved, kinds ...cache. var out []Hit for _, f := range x.searchFiles(def.File) { - pf, err := x.ss.Parse(ctx, f) + pf, err := x.view.Parse(ctx, f) if err != nil || pf.AST() == nil { continue } @@ -353,26 +353,21 @@ func (x *Index) ReferencesTo(ctx context.Context, def *Resolved, kinds ...cache. // ReferencingFiles returns every file that directly includes file, // in graph order. func (x *Index) ReferencingFiles(file uri.URI) []uri.URI { - return x.ss.Includers(file) + return x.view.Includers(file) } // FindInWorkspace returns the definition of name in any known file of the // workspace, falling back to a directory walk when the workspace has not // been indexed yet (e.g. a quick-fix on the first didOpen). func (x *Index) FindInWorkspace(ctx context.Context, name string) (*Resolved, error) { - view := x.ss.View() - if view == nil { - return nil, nil - } - include, identifier := splitQualifiedName(name) - for _, f := range view.KnownFiles() { + for _, f := range x.view.KnownFiles() { if include != "" && includeNameOf(f) != include { continue } - pf, err := x.ss.Parse(ctx, f) + pf, err := x.view.Parse(ctx, f) if err != nil || pf.AST() == nil { continue } @@ -385,14 +380,14 @@ func (x *Index) FindInWorkspace(ctx context.Context, name string) (*Resolved, er // Fallback to the directory walk when KnownFiles is empty: the walk // goes through the view's file source (disk, or the in-memory tree in // tests). - root := view.Folder() + root := x.view.Folder() if root == "" { return nil, nil } var files []uri.URI - err := view.WalkFiles(ctx, root, func(u uri.URI) error { + err := x.view.WalkFiles(ctx, root, func(u uri.URI) error { if strings.HasSuffix(u.Path(), ".thrift") { files = append(files, u) } @@ -410,7 +405,7 @@ func (x *Index) FindInWorkspace(ctx context.Context, name string) (*Resolved, er continue } - pf, err := x.ss.Parse(ctx, f) + pf, err := x.view.Parse(ctx, f) if err != nil || pf.AST() == nil { continue } @@ -526,7 +521,7 @@ func enumSegmentHit(pf *cache.ParsedFile, node syntax.Node, off int, seg string) func (x *Index) searchFiles(file uri.URI) []uri.URI { files := []uri.URI{file} - for _, dep := range x.ss.Dependents(file) { + for _, dep := range x.view.Dependents(file) { if dep != file { files = append(files, dep) } diff --git a/lsp/source/index_memo_test.go b/lsp/source/index_memo_test.go index 6857031..52b697f 100644 --- a/lsp/source/index_memo_test.go +++ b/lsp/source/index_memo_test.go @@ -16,7 +16,7 @@ import ( // identical, unresolved names stay unresolved, and the same name resolved // as a type versus a value does not collide. func TestIndexResolutionMemo(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///base.thrift", Version: 0, @@ -31,8 +31,8 @@ func TestIndexResolutionMemo(t *testing.T) { }, }) - ix := NewIndex(ss) - pf := parseOne(t, ss, fu("/app.thrift")) + ix := NewIndex(view) + pf := parseOne(t, view, fu("/app.thrift")) tests := []struct { name string @@ -123,7 +123,7 @@ func TestIndexResolutionMemo(t *testing.T) { // resolved from different files never collides, and neither do different // names from the same file. func TestIndexMemoKeyIsolation(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///a.thrift", Version: 0, @@ -144,9 +144,9 @@ func TestIndexMemoKeyIsolation(t *testing.T) { }, }) - ix := NewIndex(ss) - main := parseOne(t, ss, fu("/main.thrift")) - fromB := parseOne(t, ss, fu("/b.thrift")) + ix := NewIndex(view) + main := parseOne(t, view, fu("/main.thrift")) + fromB := parseOne(t, view, fu("/b.thrift")) tests := []struct { name string @@ -188,7 +188,7 @@ func TestSemanticAnalysisSkipsBrokenFile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///f.thrift", Version: 0, @@ -197,7 +197,7 @@ func TestSemanticAnalysisSkipsBrokenFile(t *testing.T) { }, }) - got, err := (&SemanticAnalysis{}).Diagnostic(t.Context(), ss, []uri.URI{"file:///f.thrift"}) + got, err := (&SemanticAnalysis{}).Diagnostic(t.Context(), view, []uri.URI{"file:///f.thrift"}) require.NoError(t, err, "a broken file must not fail the diagnostics run") assert.NotNil(t, got) }) diff --git a/lsp/source/index_test.go b/lsp/source/index_test.go index 4bf7c6f..0abfba3 100644 --- a/lsp/source/index_test.go +++ b/lsp/source/index_test.go @@ -13,32 +13,32 @@ import ( func TestIndex_ResolveType_SameFile(t *testing.T) { ctx := t.Context() - ss := snap(t, "/t.thrift", "struct Foo {}\ntypedef i32 Age") - from := parseOne(t, ss, fu("/t.thrift")) + view := snap(t, "/t.thrift", "struct Foo {}\ntypedef i32 Age") + from := parseOne(t, view, fu("/t.thrift")) - def, err := NewIndex(ss).ResolveType(ctx, from, ft("Foo")) + def, err := NewIndex(view).ResolveType(ctx, from, ft("Foo")) require.NoError(t, err) require.NotNil(t, def) assert.Equal(t, fu("/t.thrift"), def.File) assert.Equal(t, DefinitionStruct, def.Kind) - def2, err := NewIndex(ss).ResolveType(ctx, from, ft("Age")) + def2, err := NewIndex(view).ResolveType(ctx, from, ft("Age")) require.NoError(t, err) require.NotNil(t, def2) assert.Equal(t, DefinitionTypedef, def2.Kind) - def3, err := NewIndex(ss).ResolveType(ctx, from, ft("i32")) + def3, err := NewIndex(view).ResolveType(ctx, from, ft("i32")) require.NoError(t, err) assert.Nil(t, def3) } func TestIndex_ResolveType_IncludeChain(t *testing.T) { ctx := t.Context() - ss := crossSnap(t, "/a.thrift", `include "b.thrift" + view := crossSnap(t, "/a.thrift", `include "b.thrift" struct Foo { 1: b.Bar bar, }`, "/b.thrift", "struct Bar {}") - a := parseOne(t, ss, fu("/a.thrift")) + a := parseOne(t, view, fu("/a.thrift")) - def, err := NewIndex(ss).ResolveType(ctx, a, ft("b.Bar")) + def, err := NewIndex(view).ResolveType(ctx, a, ft("b.Bar")) require.NoError(t, err) require.NotNil(t, def) assert.Equal(t, fu("/b.thrift"), def.File) @@ -48,33 +48,33 @@ struct Foo { 1: b.Bar bar, }`, "/b.thrift", "struct Bar {}") func TestIndex_ResolveValue(t *testing.T) { ctx := t.Context() - ss := crossSnap(t, "/a.thrift", `include "b.thrift" + view := crossSnap(t, "/a.thrift", `include "b.thrift" const i32 C = b.MAX`, "/b.thrift", "const i32 MAX = 10\nenum Color { RED }") - a := parseOne(t, ss, fu("/a.thrift")) + a := parseOne(t, view, fu("/a.thrift")) - def, err := NewIndex(ss).ResolveValue(ctx, a, cv("b.MAX")) + def, err := NewIndex(view).ResolveValue(ctx, a, cv("b.MAX")) require.NoError(t, err) require.NotNil(t, def) assert.Equal(t, fu("/b.thrift"), def.File) assert.Equal(t, DefinitionConst, def.Kind) // RED is defined in b.thrift, resolved through the include chain. - def2, err := NewIndex(ss).ResolveValue(ctx, a, cv("RED")) + def2, err := NewIndex(view).ResolveValue(ctx, a, cv("RED")) require.NoError(t, err) require.NotNil(t, def2) assert.Equal(t, fu("/b.thrift"), def2.File) assert.Equal(t, DefinitionEnumValue, def2.Kind) - def3, err := NewIndex(ss).ResolveValue(ctx, a, cv("true")) + def3, err := NewIndex(view).ResolveValue(ctx, a, cv("true")) require.NoError(t, err) assert.Nil(t, def3) } func TestIndex_ResolveService(t *testing.T) { ctx := t.Context() - ss := snap(t, "/t.thrift", "service Base {}") - a := parseOne(t, ss, fu("/t.thrift")) - def, err := NewIndex(ss).ResolveService(ctx, a, &syntax.Identifier{Text: "Base"}) + view := snap(t, "/t.thrift", "service Base {}") + a := parseOne(t, view, fu("/t.thrift")) + def, err := NewIndex(view).ResolveService(ctx, a, &syntax.Identifier{Text: "Base"}) require.NoError(t, err) require.NotNil(t, def) assert.Equal(t, DefinitionService, def.Kind) @@ -82,20 +82,20 @@ func TestIndex_ResolveService(t *testing.T) { func TestIndex_References_Type(t *testing.T) { ctx := t.Context() - ss := snap(t, "/t.thrift", "struct User {}\nstruct Foo { 1: User user, 2: list users, }\nservice Svc { User get(1: i32 id); }") - _ = parseOne(t, ss, fu("/t.thrift")) + view := snap(t, "/t.thrift", "struct User {}\nstruct Foo { 1: User user, 2: list users, }\nservice Svc { User get(1: i32 id); }") + _ = parseOne(t, view, fu("/t.thrift")) - hits, err := NewIndex(ss).References(ctx, fu("/t.thrift"), "User", cache.RefFieldType, cache.RefSignatureType) + hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "User", cache.RefFieldType, cache.RefSignatureType) require.NoError(t, err) require.Len(t, hits, 3) } func TestIndex_References_ExceptionRule(t *testing.T) { ctx := t.Context() - ss := snap(t, "/t.thrift", "exception Bad {}\nstruct Foo { 1: Bad bad, }\nservice Svc { void f() throws (1: Bad e); }") - _ = parseOne(t, ss, fu("/t.thrift")) + view := snap(t, "/t.thrift", "exception Bad {}\nstruct Foo { 1: Bad bad, }\nservice Svc { void f() throws (1: Bad e); }") + _ = parseOne(t, view, fu("/t.thrift")) - hits, err := NewIndex(ss).References(ctx, fu("/t.thrift"), "Bad", cache.RefSignatureType) + hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "Bad", cache.RefSignatureType) require.NoError(t, err) require.Len(t, hits, 1) assert.Equal(t, "Bad", hits[0].Text) @@ -103,24 +103,24 @@ func TestIndex_References_ExceptionRule(t *testing.T) { func TestIndex_References_ConstValue(t *testing.T) { ctx := t.Context() - ss := snap(t, "/t.thrift", "const i32 MAX = 10\nstruct Foo { 1: i32 id = MAX, }") - _ = parseOne(t, ss, fu("/t.thrift")) + view := snap(t, "/t.thrift", "const i32 MAX = 10\nstruct Foo { 1: i32 id = MAX, }") + _ = parseOne(t, view, fu("/t.thrift")) - hits, err := NewIndex(ss).References(ctx, fu("/t.thrift"), "MAX", cache.RefConstValue) + hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "MAX", cache.RefConstValue) require.NoError(t, err) require.Len(t, hits, 1) } func TestIndex_ReferencesToEnumValues(t *testing.T) { ctx := t.Context() - ss := snap(t, "/t.thrift", "enum Color { RED = 0, BLUE = 1 }\nstruct Foo { 1: i32 id = Color.RED, }\nconst i32 C = Color.BLUE") - pf := parseOne(t, ss, fu("/t.thrift")) + view := snap(t, "/t.thrift", "enum Color { RED = 0, BLUE = 1 }\nstruct Foo { 1: i32 id = Color.RED, }\nconst i32 C = Color.BLUE") + pf := parseOne(t, view, fu("/t.thrift")) - def, err := NewIndex(ss).ResolveType(ctx, pf, ft("Color")) + def, err := NewIndex(view).ResolveType(ctx, pf, ft("Color")) require.NoError(t, err) require.NotNil(t, def) - hits, err := NewIndex(ss).ReferencesTo(ctx, def, cache.RefFieldType, cache.RefSignatureType, cache.RefConstValue) + hits, err := NewIndex(view).ReferencesTo(ctx, def, cache.RefFieldType, cache.RefSignatureType, cache.RefConstValue) require.NoError(t, err) require.Len(t, hits, 2) for _, h := range hits { @@ -129,9 +129,9 @@ func TestIndex_ReferencesToEnumValues(t *testing.T) { } func TestIndex_ReferencingFiles(t *testing.T) { - ss := crossSnap(t, "/a.thrift", `include "b.thrift"`, "/b.thrift", "") - _ = parseOne(t, ss, fu("/a.thrift")) - files := NewIndex(ss).ReferencingFiles(fu("/b.thrift")) + view := crossSnap(t, "/a.thrift", `include "b.thrift"`, "/b.thrift", "") + _ = parseOne(t, view, fu("/a.thrift")) + files := NewIndex(view).ReferencingFiles(fu("/b.thrift")) require.Len(t, files, 1) assert.Equal(t, fu("/a.thrift"), files[0]) } @@ -149,11 +149,11 @@ func TestIndex_FindInWorkspace(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := t.Context() - ss := crossSnap(t, "/a.thrift", "struct User {}", tt.file, "struct Account {}") - _ = parseOne(t, ss, fu("/a.thrift")) - _ = parseOne(t, ss, fu(tt.file)) + view := crossSnap(t, "/a.thrift", "struct User {}", tt.file, "struct Account {}") + _ = parseOne(t, view, fu("/a.thrift")) + _ = parseOne(t, view, fu(tt.file)) - def, err := NewIndex(ss).FindInWorkspace(ctx, tt.query) + def, err := NewIndex(view).FindInWorkspace(ctx, tt.query) require.NoError(t, err) require.NotNil(t, def) assert.Equal(t, fu(tt.file), def.File) @@ -171,29 +171,29 @@ func TestRefKindsFor(t *testing.T) { // --- helpers --- -func snap(t *testing.T, file, content string) *cache.Snapshot { +func snap(t *testing.T, file, content string) *cache.View { t.Helper() - return cache.BuildSnapshotForTest([]*cache.FileChange{{ + return cache.BuildViewForTest([]*cache.FileChange{{ URI: fu(file), Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen, }}) } // crossSnap builds a snapshot with two files, parsed in dependency order // (includes first), so the include graph resolves correctly. -func crossSnap(t *testing.T, fa, ca, fb, cb string) *cache.Snapshot { +func crossSnap(t *testing.T, fa, ca, fb, cb string) *cache.View { t.Helper() - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: fu(fb), Version: 0, Content: []byte(cb), From: cache.FileChangeTypeDidOpen}, {URI: fu(fa), Version: 0, Content: []byte(ca), From: cache.FileChangeTypeDidOpen}, }) - return ss + return view } func fu(p string) uri.URI { u, _ := uri.Parse("file://" + p); return u } -func parseOne(t *testing.T, ss *cache.Snapshot, u uri.URI) *cache.ParsedFile { +func parseOne(t *testing.T, view *cache.View, u uri.URI) *cache.ParsedFile { t.Helper() - pf, err := ss.Parse(t.Context(), u) + pf, err := view.Parse(t.Context(), u) require.NoError(t, err) return pf } diff --git a/lsp/source/links.go b/lsp/source/links.go index 2096dfd..3a2b317 100644 --- a/lsp/source/links.go +++ b/lsp/source/links.go @@ -1,5 +1,5 @@ // Package links computes document links: include paths resolving to their -// target files. Pure over the snapshot: parsing and file I/O happen in the +// target files. Pure over the view: parsing and file I/O happen in the // caller. package source @@ -16,14 +16,14 @@ import ( // Links returns the document links of a file, one per include and // cpp_include, targeting the resolved file. -func Links(ctx context.Context, ss *cache.Snapshot, file uri.URI) []protocol.DocumentLink { - pf, err := ss.Parse(ctx, file) +func Links(ctx context.Context, view *cache.View, file uri.URI) []protocol.DocumentLink { + pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil } doc := pf.AST() - resolver := ss.Resolver() + resolver := view.Resolver() var out []protocol.DocumentLink diff --git a/lsp/source/links_test.go b/lsp/source/links_test.go index 064f3d1..4f08575 100644 --- a/lsp/source/links_test.go +++ b/lsp/source/links_test.go @@ -11,14 +11,14 @@ import ( ) // buildSnapshot parses src as the file at URI and returns the snapshot. -func buildLinksSnapshot(t *testing.T, file uri.URI, src string) *cache.Snapshot { +func buildLinksSnapshot(t *testing.T, file uri.URI, src string) *cache.View { t.Helper() - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: file, Version: 0, Content: []byte(src), From: cache.FileChangeTypeDidOpen}, }) - return ss + return view } func TestLinks(t *testing.T) { @@ -53,9 +53,9 @@ struct S {}`, for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := buildLinksSnapshot(t, "file:///tmp/main.thrift", tt.src) + view := buildLinksSnapshot(t, "file:///tmp/main.thrift", tt.src) - got := Links(t.Context(), ss, "file:///tmp/main.thrift") + got := Links(t.Context(), view, "file:///tmp/main.thrift") if tt.want == nil { assert.Empty(t, got) @@ -76,9 +76,9 @@ struct S {}`, // TestLinksRange pins the link range to the include string literal. func TestLinksRange(t *testing.T) { - ss := buildLinksSnapshot(t, "file:///tmp/main.thrift", "include \"base.thrift\"\n") + view := buildLinksSnapshot(t, "file:///tmp/main.thrift", "include \"base.thrift\"\n") - got := Links(t.Context(), ss, "file:///tmp/main.thrift") + got := Links(t.Context(), view, "file:///tmp/main.thrift") require.Len(t, got, 1) assert.Equal(t, uint32(0), got[0].Range.Start.Line) diff --git a/lsp/source/on_type_format_test.go b/lsp/source/on_type_format_test.go index 2de73f8..efb7d7e 100644 --- a/lsp/source/on_type_format_test.go +++ b/lsp/source/on_type_format_test.go @@ -18,7 +18,7 @@ func TestOnTypeFormat(t *testing.T) { // The closing brace was just typed at the end of the document. pos := protocol.Position{Line: 0, Character: uint32(len(src) - 1)} - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/f.thrift", Version: 0, @@ -26,10 +26,10 @@ func TestOnTypeFormat(t *testing.T) { From: cache.FileChangeTypeDidOpen, }, }) - fh, err := ss.ReadFile(t.Context(), "file:///tmp/f.thrift") + fh, err := view.ReadFile(t.Context(), "file:///tmp/f.thrift") require.NoError(t, err) - edits, err := OnTypeFormat(t.Context(), ss, fh, formatter.DefaultOptions(), pos) + edits, err := OnTypeFormat(t.Context(), view, fh, formatter.DefaultOptions(), pos) require.NoError(t, err) require.Len(t, edits, 1) @@ -43,7 +43,7 @@ func TestOnTypeFormatSkipsBrokenDocument(t *testing.T) { src := "struct S { 1: " pos := protocol.Position{Line: 0, Character: uint32(len(src))} - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/f.thrift", Version: 0, @@ -51,10 +51,10 @@ func TestOnTypeFormatSkipsBrokenDocument(t *testing.T) { From: cache.FileChangeTypeDidOpen, }, }) - fh, err := ss.ReadFile(t.Context(), "file:///tmp/f.thrift") + fh, err := view.ReadFile(t.Context(), "file:///tmp/f.thrift") require.NoError(t, err) - edits, err := OnTypeFormat(t.Context(), ss, fh, formatter.DefaultOptions(), pos) + edits, err := OnTypeFormat(t.Context(), view, fh, formatter.DefaultOptions(), pos) require.NoError(t, err) assert.Empty(t, edits) } @@ -64,7 +64,7 @@ func TestOnTypeFormatSkipsBrokenDocument(t *testing.T) { func TestFormatSkipsBrokenDocument(t *testing.T) { src := "struct S { 1: " - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/f.thrift", Version: 0, @@ -72,10 +72,10 @@ func TestFormatSkipsBrokenDocument(t *testing.T) { From: cache.FileChangeTypeDidOpen, }, }) - fh, err := ss.ReadFile(t.Context(), "file:///tmp/f.thrift") + fh, err := view.ReadFile(t.Context(), "file:///tmp/f.thrift") require.NoError(t, err) - edit, err := FormatDocument(t.Context(), ss, fh, formatter.DefaultOptions()) + edit, err := FormatDocument(t.Context(), view, fh, formatter.DefaultOptions()) require.NoError(t, err) assert.Nil(t, edit) } diff --git a/lsp/source/parse.go b/lsp/source/parse.go index ad7494b..cec1d99 100644 --- a/lsp/source/parse.go +++ b/lsp/source/parse.go @@ -14,13 +14,13 @@ import ( type Parse struct{} -func (p *Parse) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (p *Parse) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { var errs []error res := make(DiagnosticResult) for _, uri := range changeFiles { - parseRes, err := ss.Parse(ctx, uri) + parseRes, err := view.Parse(ctx, uri) if err != nil { errs = append(errs, err) diff --git a/lsp/source/provider.go b/lsp/source/provider.go index 0c68580..64d5f31 100644 --- a/lsp/source/provider.go +++ b/lsp/source/provider.go @@ -18,7 +18,7 @@ import ( type Provider interface { // Candidates returns unfiltered candidates for the slot. The current // context carries the prefix and the document. - Candidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate + Candidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate } // providersFor returns the providers for a slot: the exact slot provider @@ -53,34 +53,34 @@ func providersFor(kind ContextKind) []Provider { type includeProvider struct{} -func (includeProvider) Candidates(_ context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate { - return ListDirAndFiles(filepath.Dir(file.FsPath()), ss.Resolver().IncludePaths(), c.Prefix) +func (includeProvider) Candidates(_ context.Context, view *cache.View, file uri.URI, c Context) []Candidate { + return ListDirAndFiles(filepath.Dir(file.FsPath()), view.Resolver().IncludePaths(), c.Prefix) } type typeProvider struct{} -func (typeProvider) Candidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate { - return typeCandidates(ctx, ss, file, c) +func (typeProvider) Candidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate { + return typeCandidates(ctx, view, file, c) } type valueProvider struct{} -func (valueProvider) Candidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate { - return valueCandidates(ctx, ss, file, c.Doc) +func (valueProvider) Candidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate { + return valueCandidates(ctx, view, file, c.Doc) } type keywordProvider struct{} // Candidates returns the keyword snippets and every identifier token known // to the file (and its includes). -func (keywordProvider) Candidates(_ context.Context, ss *cache.Snapshot, file uri.URI, _ Context) []Candidate { +func (keywordProvider) Candidates(_ context.Context, view *cache.View, file uri.URI, _ Context) []Candidate { res := make([]Candidate, 0, len(keywords)+16) for text, format := range keywords { res = append(res, Candidate{showText: text, insertText: text, format: format}) } - for text := range ss.TokensForFile(file) { + for text := range view.TokensForFile(file) { res = append(res, Candidate{showText: text, insertText: text, format: protocol.InsertTextFormatPlainText}) } @@ -92,13 +92,13 @@ type fieldNameProvider struct{} // Candidates returns the field modifiers and every identifier token, so a // field name position suggests required/optional and names from the // codebase — never value candidates. -func (fieldNameProvider) Candidates(_ context.Context, ss *cache.Snapshot, file uri.URI, _ Context) []Candidate { +func (fieldNameProvider) Candidates(_ context.Context, view *cache.View, file uri.URI, _ Context) []Candidate { res := []Candidate{ {showText: "required", insertText: "required", format: protocol.InsertTextFormatPlainText}, {showText: "optional", insertText: "optional", format: protocol.InsertTextFormatPlainText}, } - for text := range ss.TokensForFile(file) { + for text := range view.TokensForFile(file) { res = append(res, Candidate{showText: text, insertText: text, format: protocol.InsertTextFormatPlainText}) } @@ -109,7 +109,7 @@ type annotationKeyProvider struct{} // Candidates collects the annotation names used in the file and its // transitively included files. -func (annotationKeyProvider) Candidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate { +func (annotationKeyProvider) Candidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate { keys := make(map[string]struct{}) collect := func(doc *syntax.Document) { @@ -120,8 +120,8 @@ func (annotationKeyProvider) Candidates(ctx context.Context, ss *cache.Snapshot, collect(c.Doc) - for _, inc := range includedFiles(ss, file) { - if pf, err := ss.Parse(ctx, inc); err == nil && pf.AST() != nil { + for _, inc := range includedFiles(view, file) { + if pf, err := view.Parse(ctx, inc); err == nil && pf.AST() != nil { collect(pf.AST()) } } @@ -182,7 +182,7 @@ func annotationKeys(doc *syntax.Document) map[string]struct{} { type serviceExtendsProvider struct{} // Candidates returns the service names from the file and its includes. -func (serviceExtendsProvider) Candidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate { +func (serviceExtendsProvider) Candidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate { names := make(map[string]struct{}) collect := func(doc *syntax.Document) { @@ -193,8 +193,8 @@ func (serviceExtendsProvider) Candidates(ctx context.Context, ss *cache.Snapshot collect(c.Doc) - for _, inc := range includedFiles(ss, file) { - if pf, err := ss.Parse(ctx, inc); err == nil && pf.AST() != nil { + for _, inc := range includedFiles(view, file) { + if pf, err := view.Parse(ctx, inc); err == nil && pf.AST() != nil { collect(pf.AST()) } } diff --git a/lsp/source/reference.go b/lsp/source/reference.go index dc7bbd9..b6cb0b7 100644 --- a/lsp/source/reference.go +++ b/lsp/source/reference.go @@ -23,8 +23,8 @@ var highlightKind = map[cache.RefKind]protocol.DocumentHighlightKind{ // Reference returns every usage of the symbol at pos, including usage // in files that include the definition. -func Reference(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) ([]protocol.Location, error) { - refs, err := searchReferences(ctx, ss, file, pos) +func Reference(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) ([]protocol.Location, error) { + refs, err := searchReferences(ctx, view, file, pos) if err != nil { return nil, err } @@ -42,13 +42,13 @@ func Reference(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protoc } // Highlight returns the document highlight ranges for the symbol at pos. -func Highlight(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) ([]protocol.DocumentHighlight, error) { - pf, target, err := resolveTarget(ctx, ss, file, pos) +func Highlight(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) ([]protocol.DocumentHighlight, error) { + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return nil, err } - refs, err := searchReferences(ctx, ss, file, pos) + refs, err := searchReferences(ctx, view, file, pos) if err != nil { return nil, err } @@ -93,30 +93,30 @@ func Highlight(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protoc } // searchReferences dispatches to the reference search for the target kind. -func searchReferences(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) ([]indexHit, error) { - pf, target, err := resolveTarget(ctx, ss, file, pos) +func searchReferences(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) ([]indexHit, error) { + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return nil, err } - ix := NewIndex(ss) + ix := NewIndex(view) switch target.kind { case TargetTypeName: - return searchTypeNameRefs(ctx, ix, ss, pf, target) + return searchTypeNameRefs(ctx, ix, view, pf, target) case TargetConstValue: - return searchConstValueRefs(ctx, ix, ss, pf, target) + return searchConstValueRefs(ctx, ix, view, pf, target) case TargetService: - return searchServiceRefs(ctx, ix, ss, file, target.identifier().Text) + return searchServiceRefs(ctx, ix, view, file, target.identifier().Text) case TargetDefinition: - return searchDefRefs(ctx, ix, ss, file, pf, target) + return searchDefRefs(ctx, ix, view, file, pf, target) } return nil, nil } // searchTypeNameRefs resolves the type reference and finds all usages. -func searchTypeNameRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, pf *cache.ParsedFile, target *target) ([]indexHit, error) { +func searchTypeNameRefs(ctx context.Context, ix *Index, view *cache.View, pf *cache.ParsedFile, target *target) ([]indexHit, error) { ft := target.parent.(*syntax.FieldType) typeName := typeReferenceName(ft) if typeName == "" || IsBasicType(typeName) { @@ -132,7 +132,7 @@ func searchTypeNameRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, pf * return nil, nil } - loc, err := jumpInFile(ctx, ss, def.File, def.Name) + loc, err := jumpInFile(ctx, view, def.File, def.Name) if err != nil { return nil, err } @@ -152,7 +152,7 @@ func searchTypeNameRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, pf * } // searchConstValueRefs resolves a const-value or enum-value reference. -func searchConstValueRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, pf *cache.ParsedFile, target *target) ([]indexHit, error) { +func searchConstValueRefs(ctx context.Context, ix *Index, view *cache.View, pf *cache.ParsedFile, target *target) ([]indexHit, error) { value := target.node.(*syntax.ConstValue) def, err := ix.ResolveValue(ctx, pf, value) @@ -164,7 +164,7 @@ func searchConstValueRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, pf return nil, nil } - loc, err := jumpInFile(ctx, ss, def.File, def.Name) + loc, err := jumpInFile(ctx, view, def.File, def.Name) if err != nil { return nil, err } @@ -184,8 +184,8 @@ func searchConstValueRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, pf } // searchServiceRefs finds the includes and extends referencing a service. -func searchServiceRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, file uri.URI, svcName string) ([]indexHit, error) { - pf, err := ss.Parse(ctx, file) +func searchServiceRefs(ctx context.Context, ix *Index, view *cache.View, file uri.URI, svcName string) ([]indexHit, error) { + pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil, err } @@ -210,7 +210,7 @@ func searchServiceRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, file // searchDefRefs handles references from a definition name: struct, union, // exception, enum, typedef, const, enum value, and service names. -func searchDefRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile, target *target) ([]indexHit, error) { +func searchDefRefs(ctx context.Context, ix *Index, view *cache.View, file uri.URI, pf *cache.ParsedFile, target *target) ([]indexHit, error) { id := target.identifier() if id == nil { return nil, nil @@ -233,7 +233,7 @@ func searchDefRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, file uri. if strings.Contains(svcName, ".") { include, _ := parseIdent(file, pf.AST().Includes(), svcName) - resolver := ss.Resolver() + resolver := view.Resolver() if path := resolver.GetIncludePath(pf.AST(), include); path != "" { file = resolver.ResolveInclude(file, path) } @@ -241,7 +241,7 @@ func searchDefRefs(ctx context.Context, ix *Index, ss *cache.Snapshot, file uri. svcName = fmt.Sprintf("%s.%s", includeNameOf(file), svcName) } - return searchServiceRefs(ctx, ix, ss, file, svcName) + return searchServiceRefs(ctx, ix, view, file, svcName) default: kind, ok := definitionKindOf(parent) if !ok { diff --git a/lsp/source/reference_test.go b/lsp/source/reference_test.go index 07d68e5..08054cc 100644 --- a/lsp/source/reference_test.go +++ b/lsp/source/reference_test.go @@ -52,7 +52,7 @@ const user.UserType usermale = "male" const UserKind kind = "1" ` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -69,7 +69,7 @@ const UserKind kind = "1" type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View file uri.URI pos protocol.Position } @@ -84,7 +84,7 @@ const UserKind kind = "1" name: "case struct", // user.Test args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -125,7 +125,7 @@ const UserKind kind = "1" name: "case struct 2", // Test args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 0, @@ -153,7 +153,7 @@ const UserKind kind = "1" name: "case union", // user.Test2 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -194,7 +194,7 @@ const UserKind kind = "1" name: "case union 2", // Test2 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 7, @@ -222,7 +222,7 @@ const UserKind kind = "1" name: "case enum", // user.Test3 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -276,7 +276,7 @@ const UserKind kind = "1" name: "case enum 2", // Test3 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 14, @@ -332,7 +332,7 @@ const UserKind kind = "1" name: "case exceptions", // user.Error1 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -373,7 +373,7 @@ const UserKind kind = "1" name: "case exceptions 2", // Error1 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 19, @@ -401,7 +401,7 @@ const UserKind kind = "1" name: "case typedef", // user.UserType args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -468,7 +468,7 @@ const UserKind kind = "1" name: "case typedef 2", // UserType args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 26, @@ -522,7 +522,7 @@ const UserKind kind = "1" name: "case enumvalue", // user.Test3.TWO args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -563,7 +563,7 @@ const UserKind kind = "1" name: "case enumvalue 2", // TWO args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 16, @@ -591,7 +591,7 @@ const UserKind kind = "1" name: "case const", // user.DefaultName args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -632,7 +632,7 @@ const UserKind kind = "1" name: "case const 2", // DefaultName args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 27, @@ -660,7 +660,7 @@ const UserKind kind = "1" name: "type in same file", // UserKind args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 8, @@ -700,7 +700,7 @@ const UserKind kind = "1" } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := Reference(tt.args.ctx, tt.args.ss, tt.args.file, tt.args.pos) + got, err := Reference(tt.args.ctx, tt.args.view, tt.args.file, tt.args.pos) tt.assertion(t, err) assert.Equal(t, tt.want, got) }) diff --git a/lsp/source/rename.go b/lsp/source/rename.go index d51f041..7a5a1ef 100644 --- a/lsp/source/rename.go +++ b/lsp/source/rename.go @@ -14,8 +14,8 @@ import ( // PrepareRename returns the range of the identifier under the cursor when // renaming is supported: definition names, const values, and services. -func PrepareRename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res *protocol.Range, err error) { - pf, target, err := resolveTarget(ctx, ss, file, pos) +func PrepareRename(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) (res *protocol.Range, err error) { + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return res, err } @@ -44,8 +44,8 @@ func PrepareRename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos pr // Rename renames the definition under the cursor and all its references, // preserving include qualifiers on qualified references (user.Test becomes // user.newtext, not newtext). -func Rename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position, newName string) (res *protocol.WorkspaceEdit, err error) { - pf, target, err := resolveTarget(ctx, ss, file, pos) +func Rename(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position, newName string) (res *protocol.WorkspaceEdit, err error) { + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return res, err } @@ -59,13 +59,13 @@ func Rename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol. return nil, fmt.Errorf("rename not supported for basic types") } - refs, err = searchTypeNameRefs(ctx, NewIndex(ss), ss, pf, target) + refs, err = searchTypeNameRefs(ctx, NewIndex(view), view, pf, target) if err != nil { return nil, err } case TargetConstValue: - refs, err = searchConstValueRefs(ctx, NewIndex(ss), ss, pf, target) + refs, err = searchConstValueRefs(ctx, NewIndex(view), view, pf, target) if err != nil { return nil, err } @@ -77,19 +77,19 @@ func Rename(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol. } else { include, _ := parseIdent(file, pf.AST().Includes(), svcName) - resolver := ss.Resolver() + resolver := view.Resolver() if path := resolver.GetIncludePath(pf.AST(), include); path != "" { file = resolver.ResolveInclude(file, path) } } - refs, err = searchServiceRefs(ctx, NewIndex(ss), ss, file, svcName) + refs, err = searchServiceRefs(ctx, NewIndex(view), view, file, svcName) if err != nil { return nil, err } case TargetDefinition: - refs, err = searchDefRefs(ctx, NewIndex(ss), ss, file, pf, target) + refs, err = searchDefRefs(ctx, NewIndex(view), view, file, pf, target) if err != nil { return nil, err } diff --git a/lsp/source/rename_correctness_test.go b/lsp/source/rename_correctness_test.go index a2979f3..939cb05 100644 --- a/lsp/source/rename_correctness_test.go +++ b/lsp/source/rename_correctness_test.go @@ -14,7 +14,7 @@ import ( // chain of includes (app → mid → base) is renamed everywhere it is // referenced — including in files that include it only transitively. func TestRenameTransitiveInclude(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/base.thrift", Version: 0, @@ -35,7 +35,7 @@ func TestRenameTransitiveInclude(t *testing.T) { }, }) - edit, err := Rename(t.Context(), ss, "file:///tmp/base.thrift", protocol.Position{Line: 0, Character: 7}, "Account") + edit, err := Rename(t.Context(), view, "file:///tmp/base.thrift", protocol.Position{Line: 0, Character: 7}, "Account") require.NoError(t, err) assert.Equal(t, []protocol.TextEdit{{ @@ -53,7 +53,7 @@ func TestRenameTransitiveInclude(t *testing.T) { // references to a same-named definition from another file untouched: // matches are resolved to their actual definition, not matched by name. func TestRenameResolutionMatched(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/base.thrift", Version: 0, @@ -69,7 +69,7 @@ func TestRenameResolutionMatched(t *testing.T) { }) // Rename app.thrift's own User (line 1, the definition). - edit, err := Rename(t.Context(), ss, "file:///tmp/app.thrift", protocol.Position{Line: 1, Character: 7}, "Member") + edit, err := Rename(t.Context(), view, "file:///tmp/app.thrift", protocol.Position{Line: 1, Character: 7}, "Member") require.NoError(t, err) // Only the unqualified reference and the definition change; the @@ -86,7 +86,7 @@ func TestRenameResolutionMatched(t *testing.T) { // only touches references that resolve to it: "colors.Palette.RED" must // survive a rename of the local enum's RED. func TestRenameEnumValueResolutionMatched(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/colors.thrift", Version: 0, @@ -102,7 +102,7 @@ func TestRenameEnumValueResolutionMatched(t *testing.T) { }) // Cursor on the local RED definition (line 1, char 13). - edit, err := Rename(t.Context(), ss, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 13}, "CRIMSON") + edit, err := Rename(t.Context(), view, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 13}, "CRIMSON") require.NoError(t, err) var got []string @@ -119,7 +119,7 @@ func TestRenameEnumValueResolutionMatched(t *testing.T) { // value references qualified with that enum: same-named enums in other // files are left alone. func TestRenameEnumResolutionMatched(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/colors.thrift", Version: 0, @@ -135,7 +135,7 @@ func TestRenameEnumResolutionMatched(t *testing.T) { }) // Cursor on the local Color definition (line 1, char 5). - edit, err := Rename(t.Context(), ss, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 5}, "Hue") + edit, err := Rename(t.Context(), view, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 5}, "Hue") require.NoError(t, err) var got []string diff --git a/lsp/source/rename_enum_test.go b/lsp/source/rename_enum_test.go index 3e14dcd..74113d0 100644 --- a/lsp/source/rename_enum_test.go +++ b/lsp/source/rename_enum_test.go @@ -76,9 +76,9 @@ func TestRenameEnumQualifiedValues(t *testing.T) { }) } - ss := cache.BuildSnapshotForTest(changes) + view := cache.BuildViewForTest(changes) - edit, err := Rename(t.Context(), ss, tt.cursor, tt.pos, tt.newName) + edit, err := Rename(t.Context(), view, tt.cursor, tt.pos, tt.newName) require.NoError(t, err) for file, wantNewTexts := range tt.want { diff --git a/lsp/source/rename_test.go b/lsp/source/rename_test.go index cf26f15..e0a6623 100644 --- a/lsp/source/rename_test.go +++ b/lsp/source/rename_test.go @@ -52,7 +52,7 @@ const user.UserType usermale = "male" const UserKind kind = "1" ` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -69,7 +69,7 @@ const UserKind kind = "1" type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View file uri.URI pos protocol.Position } @@ -84,7 +84,7 @@ const UserKind kind = "1" name: "case struct", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 0, @@ -107,7 +107,7 @@ const UserKind kind = "1" name: "case union", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 7, @@ -130,7 +130,7 @@ const UserKind kind = "1" name: "case enum", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 14, @@ -153,7 +153,7 @@ const UserKind kind = "1" name: "case exception", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 19, @@ -176,7 +176,7 @@ const UserKind kind = "1" name: "case type reference", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -199,7 +199,7 @@ const UserKind kind = "1" name: "case basic type reference", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -212,7 +212,7 @@ const UserKind kind = "1" name: "typedef", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 26, @@ -235,7 +235,7 @@ const UserKind kind = "1" name: "const", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 27, @@ -257,7 +257,7 @@ const UserKind kind = "1" } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotRes, err := PrepareRename(tt.args.ctx, tt.args.ss, tt.args.file, tt.args.pos) + gotRes, err := PrepareRename(tt.args.ctx, tt.args.view, tt.args.file, tt.args.pos) tt.assertion(t, err) assert.Equal(t, tt.wantRes, gotRes) }) @@ -305,7 +305,7 @@ const user.UserType usermale = "male" const UserKind kind = "1" ` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -322,7 +322,7 @@ const UserKind kind = "1" type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View file uri.URI pos protocol.Position newText string @@ -338,7 +338,7 @@ const UserKind kind = "1" name: "case struct", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 0, @@ -386,7 +386,7 @@ const UserKind kind = "1" name: "case union", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 7, @@ -434,7 +434,7 @@ const UserKind kind = "1" name: "case enum", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 14, @@ -510,7 +510,7 @@ const UserKind kind = "1" name: "case exception", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 19, @@ -558,7 +558,7 @@ const UserKind kind = "1" name: "typedef", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 26, @@ -632,7 +632,7 @@ const UserKind kind = "1" name: "const", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/user.thrift", pos: protocol.Position{ Line: 27, @@ -679,7 +679,7 @@ const UserKind kind = "1" } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - gotRes, err := Rename(tt.args.ctx, tt.args.ss, tt.args.file, tt.args.pos, tt.args.newText) + gotRes, err := Rename(tt.args.ctx, tt.args.view, tt.args.file, tt.args.pos, tt.args.newText) tt.assertion(t, err) assert.Equal(t, tt.wantRes, gotRes) }) diff --git a/lsp/source/semantic.go b/lsp/source/semantic.go index 0660315..5343b2c 100644 --- a/lsp/source/semantic.go +++ b/lsp/source/semantic.go @@ -1,6 +1,6 @@ // Package semantic computes LSP semantic tokens for a thrift document: // keywords, types, definition names, comments, strings, and numbers. -// Pure over the snapshot: parsing and file I/O happen in the caller. +// Pure over the view: parsing and file I/O happen in the caller. package source import ( @@ -47,8 +47,8 @@ func Legend() []string { // Tokens returns the delta-encoded semantic tokens of a file, in source // order. -func Tokens(ctx context.Context, ss *cache.Snapshot, file uri.URI) ([]uint32, error) { - pf, err := ss.Parse(ctx, file) +func Tokens(ctx context.Context, view *cache.View, file uri.URI) ([]uint32, error) { + pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil, err } diff --git a/lsp/source/semantic_analysis.go b/lsp/source/semantic_analysis.go index 27c3e33..dc84014 100644 --- a/lsp/source/semantic_analysis.go +++ b/lsp/source/semantic_analysis.go @@ -14,11 +14,11 @@ import ( type SemanticAnalysis struct{} -func (s *SemanticAnalysis) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (s *SemanticAnalysis) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) for _, file := range changeFiles { - items, err := s.diagnostic(ctx, ss, file) + items, err := s.diagnostic(ctx, view, file) if err != nil { return nil, err } @@ -33,8 +33,8 @@ func (s *SemanticAnalysis) Name() string { return "SemanticAnalysis" } -func (s *SemanticAnalysis) diagnostic(ctx context.Context, ss *cache.Snapshot, changeFile uri.URI) ([]protocol.Diagnostic, error) { - pf, err := ss.Parse(ctx, changeFile) +func (s *SemanticAnalysis) diagnostic(ctx context.Context, view *cache.View, changeFile uri.URI) ([]protocol.Diagnostic, error) { + pf, err := view.Parse(ctx, changeFile) if err != nil { return nil, err } @@ -52,23 +52,23 @@ func (s *SemanticAnalysis) diagnostic(ctx context.Context, ss *cache.Snapshot, c // One index per file: resolutions are memoized per (file, name), so // repeated references resolve once. - res := s.checkDefinitionExist(ctx, ss, NewIndex(ss), pf) + res := s.checkDefinitionExist(ctx, view, NewIndex(view), pf) return res, nil } // checkDefinitionExist reports field types, const values, and return types // that reference undefined definitions. -func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, ss *cache.Snapshot, ix *Index, pf *cache.ParsedFile) []protocol.Diagnostic { +func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile) []protocol.Diagnostic { ret := make([]protocol.Diagnostic, 0) processFields := func(fields []*syntax.Field) { for _, field := range fields { - items := s.checkTypeExist(ctx, ss, ix, pf, field.Type) + items := s.checkTypeExist(ctx, view, ix, pf, field.Type) ret = append(ret, items...) if field.Value != nil { - items := s.checkConstValueExist(ctx, ss, ix, pf, field.Value) + items := s.checkConstValueExist(ctx, view, ix, pf, field.Value) ret = append(ret, items...) dig := s.checkConstValueMatchType(pf, field) @@ -84,13 +84,13 @@ func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, ss *cache.S }) for _, cst := range pf.AST().Consts() { - items := s.checkConstValueExist(ctx, ss, ix, pf, cst.Value) + items := s.checkConstValueExist(ctx, view, ix, pf, cst.Value) ret = append(ret, items...) } for _, svc := range pf.AST().Services() { for _, fn := range svc.Functions { - items := s.checkTypeExist(ctx, ss, ix, pf, fn.Type) + items := s.checkTypeExist(ctx, view, ix, pf, fn.Type) ret = append(ret, items...) } } @@ -98,7 +98,7 @@ func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, ss *cache.S return ret } -func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, ss *cache.Snapshot, ix *Index, +func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile, cst *syntax.ConstValue, ) (res []protocol.Diagnostic) { if cst == nil || cst.Kind != syntax.ValueIdent { @@ -228,7 +228,7 @@ func typeName(ft *syntax.FieldType) string { return "" } -func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, ss *cache.Snapshot, ix *Index, +func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile, ft *syntax.FieldType, ) (res []protocol.Diagnostic) { if ft == nil { @@ -237,7 +237,7 @@ func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, ss *cache.Snapsho switch ft.Kind { case syntax.TypeMap, syntax.TypeList, syntax.TypeSet: - return s.checkContainerTypeExist(ctx, ix, ss, pf, ft) + return s.checkContainerTypeExist(ctx, ix, view, pf, ft) case syntax.TypeBase: return nil case syntax.TypeIdent: @@ -257,20 +257,20 @@ func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, ss *cache.Snapsho } func (s *SemanticAnalysis) checkContainerTypeExist(ctx context.Context, ix *Index, - ss *cache.Snapshot, pf *cache.ParsedFile, ft *syntax.FieldType, + view *cache.View, pf *cache.ParsedFile, ft *syntax.FieldType, ) (res []protocol.Diagnostic) { if ft.KeyType != nil { - res = append(res, s.checkTypeExist(ctx, ss, ix, pf, ft.KeyType)...) + res = append(res, s.checkTypeExist(ctx, view, ix, pf, ft.KeyType)...) if ft.Kind == syntax.TypeMap { - if dig := s.checkMapKeyScalar(ctx, ss, ix, pf, ft.KeyType); dig != nil { + if dig := s.checkMapKeyScalar(ctx, view, ix, pf, ft.KeyType); dig != nil { res = append(res, *dig) } } } if ft.ValueType != nil { - res = append(res, s.checkTypeExist(ctx, ss, ix, pf, ft.ValueType)...) + res = append(res, s.checkTypeExist(ctx, view, ix, pf, ft.ValueType)...) } return res @@ -279,8 +279,8 @@ func (s *SemanticAnalysis) checkContainerTypeExist(ctx context.Context, ix *Inde // checkMapKeyScalar returns an error when the map key type is not scalar: // thrift requires map keys to be a base type or an enum. Structs, unions, // exceptions, and containers cannot be keys; typedefs are followed. -func (s *SemanticAnalysis) checkMapKeyScalar(ctx context.Context, ss *cache.Snapshot, ix *Index, pf *cache.ParsedFile, key *syntax.FieldType) *protocol.Diagnostic { - kind := s.mapKeyKind(ctx, ss, ix, pf, key, 0) +func (s *SemanticAnalysis) checkMapKeyScalar(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile, key *syntax.FieldType) *protocol.Diagnostic { + kind := s.mapKeyKind(ctx, view, ix, pf, key, 0) if kind == "" { return nil } @@ -297,7 +297,7 @@ func (s *SemanticAnalysis) checkMapKeyScalar(ctx context.Context, ss *cache.Snap // mapKeyKind reports why key is not a scalar map key: the container kind, // or the definition kind for struct-like types. "" means scalar: a base // type, an enum, or a typedef chain ending there. -func (s *SemanticAnalysis) mapKeyKind(ctx context.Context, ss *cache.Snapshot, ix *Index, pf *cache.ParsedFile, key *syntax.FieldType, depth int) string { +func (s *SemanticAnalysis) mapKeyKind(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile, key *syntax.FieldType, depth int) string { if key == nil { return "" } @@ -333,7 +333,7 @@ func (s *SemanticAnalysis) mapKeyKind(ctx context.Context, ss *cache.Snapshot, i return "" } - return s.mapKeyKind(ctx, ss, ix, def.Parsed, td.Type, depth+1) + return s.mapKeyKind(ctx, view, ix, def.Parsed, td.Type, depth+1) } } diff --git a/lsp/source/semantic_analysis_test.go b/lsp/source/semantic_analysis_test.go index b4d6440..23220de 100644 --- a/lsp/source/semantic_analysis_test.go +++ b/lsp/source/semantic_analysis_test.go @@ -54,7 +54,7 @@ struct TestUUID { 1: required uuid id } ` - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -65,7 +65,7 @@ struct TestUUID { type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View changeFiles []uri.URI } @@ -78,8 +78,8 @@ struct TestUUID { { name: "case 1", args: args{ - ctx: t.Context(), - ss: ss, + ctx: t.Context(), + view: view, changeFiles: []uri.URI{ "file:///tmp/user.thrift", }, @@ -255,7 +255,7 @@ struct TestUUID { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &SemanticAnalysis{} - got, err := c.Diagnostic(tt.args.ctx, tt.args.ss, tt.args.changeFiles) + got, err := c.Diagnostic(tt.args.ctx, tt.args.view, tt.args.changeFiles) for key := range got { sort.SliceStable(got[key], func(i, j int) bool { @@ -323,7 +323,7 @@ func Test_SemanticAnalysis_MapKeyScalar(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ss := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -332,7 +332,7 @@ func Test_SemanticAnalysis_MapKeyScalar(t *testing.T) { }, }) - got, err := (&SemanticAnalysis{}).diagnostic(t.Context(), ss, "file:///tmp/user.thrift") + got, err := (&SemanticAnalysis{}).diagnostic(t.Context(), view, "file:///tmp/user.thrift") require.NoError(t, err) var msgs []string diff --git a/lsp/source/semantic_based_completion.go b/lsp/source/semantic_based_completion.go index 9833ab9..a9bca02 100644 --- a/lsp/source/semantic_based_completion.go +++ b/lsp/source/semantic_based_completion.go @@ -12,7 +12,7 @@ type Interface interface { // Completion returns the completion items for the request, the edit // range, and whether the list was truncated by the item cap (the LSP // isIncomplete flag). - Completion(ctx context.Context, ss *cache.Snapshot, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, bool, error) + Completion(ctx context.Context, view *cache.View, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, bool, error) } func BuildCompletionItem(candidate Candidate) *CompletionItem { diff --git a/lsp/source/semantic_completion.go b/lsp/source/semantic_completion.go index 48263da..12cb3e8 100644 --- a/lsp/source/semantic_completion.go +++ b/lsp/source/semantic_completion.go @@ -14,18 +14,18 @@ import ( // typeCandidates collects the names of all type definitions (structs, // unions, exceptions, enums, typedefs, services) from the file and its // transitively included files, plus the base type keywords. -func typeCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Context) []Candidate { +func typeCandidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate { // A dotted prefix scopes the completion to the include: suggest the // include's type names, qualified with the include name. if i := strings.LastIndexByte(c.Prefix, '.'); i >= 0 { includeName := c.Prefix[:i] - incURI := ss.Resolver().GetIncludeURI(file, c.Doc, includeName) + incURI := view.Resolver().GetIncludeURI(file, c.Doc, includeName) if incURI == "" { return nil } - pf, err := ss.Parse(ctx, incURI) + pf, err := view.Parse(ctx, incURI) if err != nil || pf.AST() == nil { return nil } @@ -54,8 +54,8 @@ func typeCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, c Con // Types from included files are suggested with their include // qualifier: a bare reference to an imported type does not resolve. - for _, inc := range includedFiles(ss, file) { - pf, err := ss.Parse(ctx, inc) + for _, inc := range includedFiles(view, file) { + pf, err := view.Parse(ctx, inc) if err != nil || pf.AST() == nil { continue } @@ -136,7 +136,7 @@ var typeKeywords = []struct { // valueCandidates collects const names and enum names and values from the // file and its transitively included files, both bare and enum-qualified. -func valueCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, doc *syntax.Document) []Candidate { +func valueCandidates(ctx context.Context, view *cache.View, file uri.URI, doc *syntax.Document) []Candidate { names := make(map[string]struct{}) collectValueNames := func(ast *syntax.Document) { for _, cst := range ast.Consts() { @@ -154,8 +154,8 @@ func valueCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, doc collectValueNames(doc) - for _, inc := range includedFiles(ss, file) { - if pf, err := ss.Parse(ctx, inc); err == nil && pf.AST() != nil { + for _, inc := range includedFiles(view, file) { + if pf, err := view.Parse(ctx, inc); err == nil && pf.AST() != nil { collectValueNames(pf.AST()) } } @@ -165,7 +165,7 @@ func valueCandidates(ctx context.Context, ss *cache.Snapshot, file uri.URI, doc // includedFiles returns the files transitively included by file, per the // include graph. -func includedFiles(ss *cache.Snapshot, file uri.URI) []uri.URI { +func includedFiles(view *cache.View, file uri.URI) []uri.URI { var out []uri.URI visited := make(map[uri.URI]bool) @@ -179,7 +179,7 @@ func includedFiles(ss *cache.Snapshot, file uri.URI) []uri.URI { visited[f] = true - for _, inc := range ss.Includes(f) { + for _, inc := range view.Includes(f) { out = append(out, inc) visit(inc) } diff --git a/lsp/source/semantic_test.go b/lsp/source/semantic_test.go index 9e8cfa8..c27ea7b 100644 --- a/lsp/source/semantic_test.go +++ b/lsp/source/semantic_test.go @@ -46,11 +46,11 @@ func decode(data []uint32) []decodedToken { func semanticTokens(t *testing.T, src string) []decodedToken { t.Helper() - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(src), From: cache.FileChangeTypeDidOpen}, }) - data, err := Tokens(t.Context(), ss, "file:///tmp/main.thrift") + data, err := Tokens(t.Context(), view, "file:///tmp/main.thrift") require.NoError(t, err) return decode(data) @@ -147,11 +147,11 @@ enum ZeonForces { // TestSemanticTokensEncoding pins the delta encoding: tokens on the same // line carry relative characters, tokens on new lines carry absolute ones. func TestSemanticTokensEncoding(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte("const i32 A = 1\nconst i32 B = 2"), From: cache.FileChangeTypeDidOpen}, }) - data, err := Tokens(t.Context(), ss, "file:///tmp/main.thrift") + data, err := Tokens(t.Context(), view, "file:///tmp/main.thrift") require.NoError(t, err) // Token 0: line 0, char 0. Token 1 (i32): same line, relative char 6. diff --git a/lsp/source/slot_completion_test.go b/lsp/source/slot_completion_test.go index de75e4f..7ce7092 100644 --- a/lsp/source/slot_completion_test.go +++ b/lsp/source/slot_completion_test.go @@ -49,13 +49,13 @@ func utf16Len(b []byte) int { // completionLabels runs the completion entry point at an LSP position and // returns the item labels, the edit range, and the truncated flag. -func completionLabels(t *testing.T, ss *cache.Snapshot, file string, pos protocol.Position) ([]string, protocol.Range, bool) { +func completionLabels(t *testing.T, view *cache.View, file string, pos protocol.Position) ([]string, protocol.Range, bool) { t.Helper() - fh, err := ss.ReadFile(t.Context(), uri.URI(file)) + fh, err := view.ReadFile(t.Context(), uri.URI(file)) assert.NoError(t, err) - items, rng, truncated, err := DefaultTokenCompletion.Completion(t.Context(), ss, &CompletionRequest{ + items, rng, truncated, err := DefaultTokenCompletion.Completion(t.Context(), view, &CompletionRequest{ Pos: pos, Fh: fh, }) @@ -70,13 +70,13 @@ func completionLabels(t *testing.T, ss *cache.Snapshot, file string, pos protoco } // completionItems runs the entry point and returns raw items. -func completionItems(t *testing.T, ss *cache.Snapshot, file string, pos protocol.Position) ([]*CompletionItem, protocol.Range, bool) { +func completionItems(t *testing.T, view *cache.View, file string, pos protocol.Position) ([]*CompletionItem, protocol.Range, bool) { t.Helper() - fh, err := ss.ReadFile(t.Context(), uri.URI(file)) + fh, err := view.ReadFile(t.Context(), uri.URI(file)) assert.NoError(t, err) - items, rng, truncated, err := DefaultTokenCompletion.Completion(t.Context(), ss, &CompletionRequest{ + items, rng, truncated, err := DefaultTokenCompletion.Completion(t.Context(), view, &CompletionRequest{ Pos: pos, Fh: fh, }) @@ -88,7 +88,7 @@ func completionItems(t *testing.T, ss *cache.Snapshot, file string, pos protocol // gundamSnapshot builds a snapshot with the gundam corpus: a main file with // a struct, enum, const, and an included file defining another type. It // returns the snapshot and the main file content. -func gundamSnapshot(t *testing.T, includePaths []string) (*cache.Snapshot, string) { +func gundamSnapshot(t *testing.T, includePaths []string) (*cache.View, string) { t.Helper() mainContent := `include "federation.gundam.thrift" @@ -112,16 +112,16 @@ exception BayFull { 1: string message }` - ss := buildSnapshot(t, includePaths, + view := buildSnapshot(t, includePaths, &cache.FileChange{URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainContent), From: cache.FileChangeTypeDidOpen}, &cache.FileChange{URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(incContent), From: cache.FileChangeTypeDidOpen}, ) - return ss, mainContent + return view, mainContent } func TestCompletionSlots(t *testing.T) { - ss, mainContent := gundamSnapshot(t, nil) + view, mainContent := gundamSnapshot(t, nil) tests := []struct { name string @@ -155,7 +155,7 @@ func TestCompletionSlots(t *testing.T) { t.Run(tt.name, func(t *testing.T) { pos := lspPosOf(t, mainContent, tt.marker) - labels, _, _ := completionLabels(t, ss, "file:///tmp/main.thrift", pos) + labels, _, _ := completionLabels(t, view, "file:///tmp/main.thrift", pos) for _, w := range tt.want { assert.Contains(t, labels, w, "labels: %v", labels) @@ -171,12 +171,12 @@ func TestCompletionSlots(t *testing.T) { // TestCompletionNoDuplicates: providers overlap (a type is both a type // candidate and an identifier token), so the shared pipeline must dedupe. func TestCompletionNoDuplicates(t *testing.T) { - ss, mainContent := gundamSnapshot(t, nil) + view, mainContent := gundamSnapshot(t, nil) for _, marker := range []string{"1: required ", "const i32 LIMIT = "} { pos := lspPosOf(t, mainContent, marker) - labels, _, _ := completionLabels(t, ss, "file:///tmp/main.thrift", pos) + labels, _, _ := completionLabels(t, view, "file:///tmp/main.thrift", pos) seen := make(map[string]struct{}, len(labels)) for _, label := range labels { @@ -191,30 +191,30 @@ func TestCompletionNoDuplicates(t *testing.T) { // value candidates on field-name positions (regression: the old code // suggested values while typing a field name). func TestCompletionSlotProviders(t *testing.T) { - ss, _ := gundamSnapshot(t, nil) + view, _ := gundamSnapshot(t, nil) - cc := Context{Doc: mustParse(t, ss, "file:///tmp/main.thrift")} + cc := Context{Doc: mustParse(t, view, "file:///tmp/main.thrift")} ctx := t.Context() - typeCands := typeProvider{}.Candidates(ctx, ss, "file:///tmp/main.thrift", cc) + typeCands := typeProvider{}.Candidates(ctx, view, "file:///tmp/main.thrift", cc) typeLabels := labelsOf(typeCands) assert.Contains(t, typeLabels, "Gundam") assert.Contains(t, typeLabels, "federation.gundam.MobileSuit", "types from included files are include-qualified") assert.NotContains(t, typeLabels, "MobileSuit", "bare imported types do not resolve") assert.Contains(t, typeLabels, "federation.gundam.BayFull", "types from included files are include-qualified") - fieldCands := fieldNameProvider{}.Candidates(ctx, ss, "file:///tmp/main.thrift", cc) + fieldCands := fieldNameProvider{}.Candidates(ctx, view, "file:///tmp/main.thrift", cc) fieldLabels := labelsOf(fieldCands) assert.Contains(t, fieldLabels, "required") assert.Contains(t, fieldLabels, "optional") assert.NotContains(t, fieldLabels, "ZeonForces.ZAKU_I", "field name slot must not suggest qualified values") - valueCands := valueProvider{}.Candidates(ctx, ss, "file:///tmp/main.thrift", cc) + valueCands := valueProvider{}.Candidates(ctx, view, "file:///tmp/main.thrift", cc) valueLabels := labelsOf(valueCands) assert.Contains(t, valueLabels, "ZeonForces.ZAKU_I", "value slot suggests qualified enum values") - keyCands := annotationKeyProvider{}.Candidates(ctx, ss, "file:///tmp/main.thrift", cc) + keyCands := annotationKeyProvider{}.Candidates(ctx, view, "file:///tmp/main.thrift", cc) keyLabels := labelsOf(keyCands) assert.Contains(t, keyLabels, "color") } @@ -228,10 +228,10 @@ func labelsOf(cands []Candidate) []string { return labels } -func mustParse(t *testing.T, ss *cache.Snapshot, file string) *syntax.Document { +func mustParse(t *testing.T, view *cache.View, file string) *syntax.Document { t.Helper() - pf, err := ss.Parse(t.Context(), uri.URI(file)) + pf, err := view.Parse(t.Context(), uri.URI(file)) assert.NoError(t, err) assert.NotNil(t, pf.AST()) @@ -247,14 +247,14 @@ func TestCompletionQualifiedValue(t *testing.T) { content := strings.Replace(mainContent, "const i32 LIMIT = 10", "const i32 LIMIT = ZeonForces.", 1) assert.NotEqual(t, mainContent, content) - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, &cache.FileChange{URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte("struct MobileSuit {\n\t1: required string ModelName\n}"), From: cache.FileChangeTypeDidOpen}, ) dotPos := lspPosOf(t, content, "const i32 LIMIT = ZeonForces.") - items, rng, _ := completionItems(t, ss, "file:///tmp/main.thrift", dotPos) + items, rng, _ := completionItems(t, view, "file:///tmp/main.thrift", dotPos) var labels []string for _, item := range items { @@ -274,11 +274,11 @@ func TestCompletionQualifiedValue(t *testing.T) { } func TestCompletionKeywordFallback(t *testing.T) { - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/empty.thrift", Version: 0, Content: []byte(""), From: cache.FileChangeTypeDidOpen}, ) - labels, _, truncated := completionLabels(t, ss, "file:///tmp/empty.thrift", protocol.Position{Line: 0, Character: 0}) + labels, _, truncated := completionLabels(t, view, "file:///tmp/empty.thrift", protocol.Position{Line: 0, Character: 0}) assert.Contains(t, labels, "include") assert.True(t, truncated, "keyword fallback exceeds the cap") } @@ -286,14 +286,14 @@ func TestCompletionKeywordFallback(t *testing.T) { // TestCompletionCapReportsIncomplete: a small result set reports the list as // complete (isIncomplete false), the keyword fallback reports truncation. func TestCompletionCapReportsIncomplete(t *testing.T) { - ss, mainContent := gundamSnapshot(t, nil) + view, mainContent := gundamSnapshot(t, nil) pos := lspPosOf(t, mainContent, "1: required ") - _, _, truncated := completionLabels(t, ss, "file:///tmp/main.thrift", pos) + _, _, truncated := completionLabels(t, view, "file:///tmp/main.thrift", pos) assert.True(t, truncated, "type slot exceeds the cap (types + keywords)") pos = lspPosOf(t, mainContent, "1: required string Name (c") - _, _, truncated = completionLabels(t, ss, "file:///tmp/main.thrift", pos) + _, _, truncated = completionLabels(t, view, "file:///tmp/main.thrift", pos) assert.False(t, truncated, "annotation key slot has one candidate") } @@ -310,11 +310,11 @@ func TestCompletionIncludePath(t *testing.T) { mainContent := "include \"fed|" pos := lspPosOf(t, mainContent, "include \"fed") - ss := buildSnapshot(t, []string{filepath.Join(dir, "zeon")}, + view := buildSnapshot(t, []string{filepath.Join(dir, "zeon")}, &cache.FileChange{URI: uri.File(filepath.Join(dir, "main.thrift")), Version: 0, Content: []byte(mainContent), From: cache.FileChangeTypeDidOpen}, ) - labels, rng, _ := completionLabels(t, ss, uri.File(filepath.Join(dir, "main.thrift")).String(), pos) + labels, rng, _ := completionLabels(t, view, uri.File(filepath.Join(dir, "main.thrift")).String(), pos) assert.Contains(t, labels, "federation.gundam.thrift") // The edit range starts after the opening quote: "include \"" is 9 @@ -335,11 +335,11 @@ func TestCompletionIncludePath(t *testing.T) { // TestCompletionNoPrefixUnderflow: a line without spaces must not produce a // wrapped edit range (the old whole-file prefix fallback bug). func TestCompletionNoPrefixUnderflow(t *testing.T) { - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/underflow.thrift", Version: 0, Content: []byte("const X=1"), From: cache.FileChangeTypeDidOpen}, ) - _, rng, _ := completionLabels(t, ss, "file:///tmp/underflow.thrift", protocol.Position{Line: 0, Character: 9}) + _, rng, _ := completionLabels(t, view, "file:///tmp/underflow.thrift", protocol.Position{Line: 0, Character: 9}) assert.LessOrEqual(t, rng.Start.Character, uint32(9), "edit range must not wrap") } @@ -349,10 +349,10 @@ func TestCompletionNonASCIIPrefix(t *testing.T) { content := "// モビルスーツ\nconst X=1 😀" pos := lspPosOf(t, content, "const X=1 😀") - ss := buildSnapshot(t, nil, + view := buildSnapshot(t, nil, &cache.FileChange{URI: "file:///tmp/emoji.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, ) - _, rng, _ := completionLabels(t, ss, "file:///tmp/emoji.thrift", pos) + _, rng, _ := completionLabels(t, view, "file:///tmp/emoji.thrift", pos) assert.Equal(t, pos.Character, rng.Start.Character, "empty prefix: range starts at the cursor") } diff --git a/lsp/source/target.go b/lsp/source/target.go index 650a742..b0f1935 100644 --- a/lsp/source/target.go +++ b/lsp/source/target.go @@ -40,8 +40,8 @@ type target struct { var errNoAST = errors.New("parse ast failed") // resolveTarget parses the file and finds what the cursor is on. -func resolveTarget(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (*cache.ParsedFile, *target, error) { - pf, err := ss.Parse(ctx, file) +func resolveTarget(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) (*cache.ParsedFile, *target, error) { + pf, err := view.Parse(ctx, file) if err != nil { return nil, nil, err } @@ -119,8 +119,8 @@ func jump(file uri.URI, pf *cache.ParsedFile, node syntax.Node) protocol.Locatio // file's AST. Use this for nodes resolved from a different file than the // one under the cursor: the node's token indices are only meaningful in its // own document's token stream. -func jumpInFile(ctx context.Context, ss *cache.Snapshot, file uri.URI, node syntax.Node) (protocol.Location, error) { - pf, err := ss.Parse(ctx, file) +func jumpInFile(ctx context.Context, view *cache.View, file uri.URI, node syntax.Node) (protocol.Location, error) { + pf, err := view.Parse(ctx, file) if err != nil { return protocol.Location{}, err } diff --git a/lsp/source/target_test.go b/lsp/source/target_test.go index 9170b7c..0882ab8 100644 --- a/lsp/source/target_test.go +++ b/lsp/source/target_test.go @@ -26,7 +26,7 @@ service Svc extends Base { User getUser(1: i64 id, 2: string name) throws (NotFound e) } ` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/test.thrift", Version: 0, @@ -79,7 +79,7 @@ service Svc extends Base { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, target, err := resolveTarget(t.Context(), ss, "file:///tmp/test.thrift", posOf(tt.marker, tt.offset)) + _, target, err := resolveTarget(t.Context(), view, "file:///tmp/test.thrift", posOf(tt.marker, tt.offset)) if err != nil { t.Fatalf("resolveTarget: %v", err) } @@ -92,7 +92,7 @@ service Svc extends Base { } func TestResolveTargetNoNode(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/test.thrift", Version: 0, @@ -102,7 +102,7 @@ func TestResolveTargetNoNode(t *testing.T) { }) // Position in the blank line between definitions: resolves to the // document itself with no target kind. - _, target, err := resolveTarget(t.Context(), ss, "file:///tmp/test.thrift", protocol.Position{Line: 3, Character: 0}) + _, target, err := resolveTarget(t.Context(), view, "file:///tmp/test.thrift", protocol.Position{Line: 3, Character: 0}) if err != nil { t.Fatalf("resolveTarget: %v", err) } diff --git a/lsp/source/token_completion.go b/lsp/source/token_completion.go index 0df0b7b..d659f83 100644 --- a/lsp/source/token_completion.go +++ b/lsp/source/token_completion.go @@ -58,8 +58,8 @@ type Candidate struct { // Completion resolves the grammar slot at the cursor and returns the // candidates for that slot, the edit range, and whether the list was // truncated by the cap. -func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, bool, error) { - parsedFile, err := ss.Parse(ctx, cmp.Fh.URI()) +func (c *TokenCompletion) Completion(ctx context.Context, view *cache.View, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, bool, error) { + parsedFile, err := view.Parse(ctx, cmp.Fh.URI()) if err != nil { return nil, protocol.Range{}, false, err } @@ -110,7 +110,7 @@ func (c *TokenCompletion) Completion(ctx context.Context, ss *cache.Snapshot, cm var candidates []Candidate for _, p := range providersFor(cc.Kind) { - candidates = append(candidates, p.Candidates(ctx, ss, cmp.Fh.URI(), cc)...) + candidates = append(candidates, p.Candidates(ctx, view, cmp.Fh.URI(), cc)...) } // Shared pipeline: prefix filter, dedupe, sort, cap. diff --git a/lsp/source/type_definition.go b/lsp/source/type_definition.go index f7a84b8..e63cbfa 100644 --- a/lsp/source/type_definition.go +++ b/lsp/source/type_definition.go @@ -14,23 +14,23 @@ import ( // under the cursor: for a type reference, the type's definition; for a // field, function, typedef, or const name, the definition of its declared // type. -func TypeDefinition(ctx context.Context, ss *cache.Snapshot, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { +func TypeDefinition(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) - pf, target, err := resolveTarget(ctx, ss, file, pos) + pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return res, err } switch target.kind { case TargetTypeName: - return typeNameDefinition(ctx, NewIndex(ss), pf, target) + return typeNameDefinition(ctx, NewIndex(view), pf, target) case TargetConstValue: // The type definition of a constant value is the value's own // definition: the enum value or const it references. - return constValueDefinition(ctx, NewIndex(ss), pf, target) + return constValueDefinition(ctx, NewIndex(view), pf, target) case TargetDefinition: - return declarationTypeDefinition(ctx, NewIndex(ss), pf, target) + return declarationTypeDefinition(ctx, NewIndex(view), pf, target) } return res, err @@ -61,7 +61,7 @@ func declarationTypeDefinition(ctx context.Context, ix *Index, pf *cache.ParsedF return nil, err } - loc, err := jumpInFile(ctx, ix.ss, def.File, def.Name) + loc, err := jumpInFile(ctx, ix.view, def.File, def.Name) if err != nil { return nil, err } diff --git a/lsp/source/type_definition_test.go b/lsp/source/type_definition_test.go index 4d27dc6..4bb9bc7 100644 --- a/lsp/source/type_definition_test.go +++ b/lsp/source/type_definition_test.go @@ -51,7 +51,7 @@ typedef user.UserType UserKind const user.UserType usermale = "male" ` - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, @@ -68,7 +68,7 @@ const user.UserType usermale = "male" type args struct { ctx context.Context - ss *cache.Snapshot + view *cache.View file uri.URI pos protocol.Position } @@ -83,7 +83,7 @@ const user.UserType usermale = "male" name: "case struct", // user.Test args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -111,7 +111,7 @@ const user.UserType usermale = "male" name: "case struct 2", // Api args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -139,7 +139,7 @@ const user.UserType usermale = "male" name: "case union", // user.Test2 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -167,7 +167,7 @@ const user.UserType usermale = "male" name: "case union 2", // arg1 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -195,7 +195,7 @@ const user.UserType usermale = "male" name: "case enum", // user.Test3 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -223,7 +223,7 @@ const user.UserType usermale = "male" name: "case enum 2", // arg2 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -251,7 +251,7 @@ const user.UserType usermale = "male" name: "case exceptions", // user.Error1 args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -279,7 +279,7 @@ const user.UserType usermale = "male" name: "case exceptions 2", // err args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 2, @@ -307,7 +307,7 @@ const user.UserType usermale = "male" name: "case typedef", // user.UserType args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -335,7 +335,7 @@ const user.UserType usermale = "male" name: "case enumvalue", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -363,7 +363,7 @@ const user.UserType usermale = "male" name: "case const", args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 3, @@ -391,7 +391,7 @@ const user.UserType usermale = "male" name: "case typedef 2", // UserKind args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 6, @@ -419,7 +419,7 @@ const user.UserType usermale = "male" name: "case const 2", // usermale args: args{ ctx: t.Context(), - ss: ss, + view: view, file: "file:///tmp/api.thrift", pos: protocol.Position{ Line: 7, @@ -446,7 +446,7 @@ const user.UserType usermale = "male" } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := TypeDefinition(tt.args.ctx, tt.args.ss, tt.args.file, tt.args.pos) + got, err := TypeDefinition(tt.args.ctx, tt.args.view, tt.args.file, tt.args.pos) tt.assertion(t, err) assert.Equal(t, tt.want, got) }) diff --git a/lsp/source/unused_include_check.go b/lsp/source/unused_include_check.go index 7bbfe5b..8134dbf 100644 --- a/lsp/source/unused_include_check.go +++ b/lsp/source/unused_include_check.go @@ -22,11 +22,11 @@ func (c *UnusedIncludeCheck) Name() string { return "UnusedIncludeCheck" } -func (c *UnusedIncludeCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, changeFiles []uri.URI) (DiagnosticResult, error) { +func (c *UnusedIncludeCheck) Diagnostic(ctx context.Context, view *cache.View, changeFiles []uri.URI) (DiagnosticResult, error) { res := make(DiagnosticResult) for _, file := range changeFiles { - items, err := c.diagnostic(ctx, ss, file) + items, err := c.diagnostic(ctx, view, file) if err != nil { return nil, err } @@ -37,8 +37,8 @@ func (c *UnusedIncludeCheck) Diagnostic(ctx context.Context, ss *cache.Snapshot, return res, nil } -func (c *UnusedIncludeCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, file uri.URI) ([]protocol.Diagnostic, error) { - pf, err := ss.Parse(ctx, file) +func (c *UnusedIncludeCheck) diagnostic(ctx context.Context, view *cache.View, file uri.URI) ([]protocol.Diagnostic, error) { + pf, err := view.Parse(ctx, file) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (c *UnusedIncludeCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, slog.Debug("parse failed", "err", err) } - return unusedIncludeDiagnostics(ctx, ss, file, pf), nil + return unusedIncludeDiagnostics(ctx, view, file, pf), nil } // unusedIncludeDiagnostics warns on every include whose target file never @@ -59,13 +59,13 @@ func (c *UnusedIncludeCheck) diagnostic(ctx context.Context, ss *cache.Snapshot, // name, a constant value identifier, or a service extends clause, used // qualified ("base.Type") or unqualified (resolving through the include // chain). -func unusedIncludeDiagnostics(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile) []protocol.Diagnostic { +func unusedIncludeDiagnostics(ctx context.Context, view *cache.View, file uri.URI, pf *cache.ParsedFile) []protocol.Diagnostic { includes := pf.AST().Includes() if len(includes) == 0 { return nil } - used := usedIncludes(ctx, ss, file, pf) + used := usedIncludes(ctx, view, file, pf) var ret []protocol.Diagnostic @@ -90,8 +90,8 @@ func unusedIncludeDiagnostics(ctx context.Context, ss *cache.Snapshot, file uri. // document resolves into. Resolution goes through the per-file reference // index, which handles both qualified ("base.Type") and unqualified names // that resolve through the include chain. -func usedIncludes(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cache.ParsedFile) map[*syntax.Include]bool { - resolver := ss.Resolver() +func usedIncludes(ctx context.Context, view *cache.View, file uri.URI, pf *cache.ParsedFile) map[*syntax.Include]bool { + resolver := view.Resolver() includeByFile := make(map[uri.URI]*syntax.Include) for _, inc := range pf.AST().Includes() { @@ -102,7 +102,7 @@ func usedIncludes(ctx context.Context, ss *cache.Snapshot, file uri.URI, pf *cac used := make(map[*syntax.Include]bool) seen := make(map[string]bool) - ix := NewIndex(ss) + ix := NewIndex(view) for _, ref := range pf.Index().References() { if seen[ref.Name] { diff --git a/lsp/source/unused_include_check_test.go b/lsp/source/unused_include_check_test.go index a2f24fc..f29d283 100644 --- a/lsp/source/unused_include_check_test.go +++ b/lsp/source/unused_include_check_test.go @@ -73,7 +73,7 @@ func Test_UnusedIncludeCheck(t *testing.T) { filePath := writeThrift(t, folder, "user.thrift", tt.content) - ss := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ { URI: uri.File(filePath), Version: 0, @@ -82,7 +82,7 @@ func Test_UnusedIncludeCheck(t *testing.T) { }, }) - got, err := (&UnusedIncludeCheck{}).diagnostic(t.Context(), ss, uri.File(filePath)) + got, err := (&UnusedIncludeCheck{}).diagnostic(t.Context(), view, uri.File(filePath)) require.NoError(t, err) var msgs []string diff --git a/lsp/source/utf16_test.go b/lsp/source/utf16_test.go index 90dfe14..d12e03a 100644 --- a/lsp/source/utf16_test.go +++ b/lsp/source/utf16_test.go @@ -24,10 +24,10 @@ import ( ) // utf16Snapshot parses src as htt.thrift. -func utf16Snapshot(t *testing.T, src string) *cache.Snapshot { +func utf16Snapshot(t *testing.T, src string) *cache.View { t.Helper() - return cache.BuildSnapshotForTest([]*cache.FileChange{ + return cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte(src), From: cache.FileChangeTypeDidOpen}, }) } @@ -39,12 +39,12 @@ func utf16Snapshot(t *testing.T, src string) *cache.Snapshot { func TestDefinitionUTF16(t *testing.T) { // htt.thrift declares HTT after an emoji comment; sakuragaoka.thrift // references it. The returned definition range must use UTF-16 columns. - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte("/* 😀 */ struct HTT {}"), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/sakuragaoka.thrift", Version: 0, Content: []byte("include \"htt.thrift\"\nstruct LightMusicClub {\n 1: required HTT band\n}"), From: cache.FileChangeTypeDidOpen}, }) - locs, err := Definition(t.Context(), ss, "file:///tmp/sakuragaoka.thrift", protocol.Position{ + locs, err := Definition(t.Context(), view, "file:///tmp/sakuragaoka.thrift", protocol.Position{ Line: 2, Character: 14, // 'H' of HTT, in UTF-16 units }) @@ -60,9 +60,9 @@ func TestDefinitionUTF16(t *testing.T) { func TestDocumentSymbolsUTF16(t *testing.T) { src := "/* 😀 */ struct HTT {\n 1: required string yui\n}" - ss := utf16Snapshot(t, src) + view := utf16Snapshot(t, src) - syms := DocumentSymbols(t.Context(), ss, "file:///tmp/htt.thrift") + syms := DocumentSymbols(t.Context(), view, "file:///tmp/htt.thrift") require.Len(t, syms, 1) assert.Equal(t, uint32(16), syms[0].SelectionRange.Start.Character) @@ -85,9 +85,9 @@ func TestSemanticTokensUTF16(t *testing.T) { func TestParseErrorDiagnosticUTF16(t *testing.T) { src := `/* 😀 */ const string song = "fuwa fuwa time` - ss := utf16Snapshot(t, src) + view := utf16Snapshot(t, src) - res, err := (&Parse{}).Diagnostic(t.Context(), ss, []uri.URI{"file:///tmp/htt.thrift"}) + res, err := (&Parse{}).Diagnostic(t.Context(), view, []uri.URI{"file:///tmp/htt.thrift"}) require.NoError(t, err) diags := res["file:///tmp/htt.thrift"] @@ -102,9 +102,9 @@ func TestLinksUTF16(t *testing.T) { src := `include /* 😀 */ "htt.thrift"` file := "file:///tmp/sakuragaoka.thrift" - ss := buildLinksSnapshot(t, uri.URI(file), src) + view := buildLinksSnapshot(t, uri.URI(file), src) - links := Links(t.Context(), ss, uri.URI(file)) + links := Links(t.Context(), view, uri.URI(file)) require.Len(t, links, 1) // The path literal starts at UTF-16 column 17: 16 runes before it, one @@ -125,12 +125,12 @@ func TestFoldingUTF16(t *testing.T) { // TestRenameUTF16 exercises the same path as Definition through rename: // the workspace edit for the definition file must use UTF-16 columns. func TestRenameUTF16(t *testing.T) { - ss := cache.BuildSnapshotForTest([]*cache.FileChange{ + view := cache.BuildViewForTest([]*cache.FileChange{ {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte("/* 😀 */ struct HTT {}"), From: cache.FileChangeTypeDidOpen}, {URI: "file:///tmp/sakuragaoka.thrift", Version: 0, Content: []byte("include \"htt.thrift\"\nstruct LightMusicClub {\n 1: required HTT band\n}"), From: cache.FileChangeTypeDidOpen}, }) - edit, err := Rename(t.Context(), ss, "file:///tmp/sakuragaoka.thrift", protocol.Position{ + edit, err := Rename(t.Context(), view, "file:///tmp/sakuragaoka.thrift", protocol.Position{ Line: 2, Character: 14, }, "HoukagoTeaTime") diff --git a/lsp/source/utils.go b/lsp/source/utils.go index 20ade9d..3c6a30f 100644 --- a/lsp/source/utils.go +++ b/lsp/source/utils.go @@ -30,10 +30,10 @@ const ( // unqualified name searches the current file first, then every file // transitively included, so a type visible through a multi-hop include // chain (A includes B includes C) is found. -func definitionFiles(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast *syntax.Document, name string) []uri.URI { +func definitionFiles(ctx context.Context, view *cache.View, file uri.URI, ast *syntax.Document, name string) []uri.URI { include, _ := parseIdent(file, ast.Includes(), name) if include != "" { - resolver := ss.Resolver() + resolver := view.Resolver() path := resolver.GetIncludePath(ast, include) if path == "" { @@ -46,7 +46,7 @@ func definitionFiles(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast files := []uri.URI{file} seen := map[uri.URI]bool{file: true} - resolver := ss.Resolver() + resolver := view.Resolver() var visit func(f uri.URI) @@ -54,7 +54,7 @@ func definitionFiles(ctx context.Context, ss *cache.Snapshot, file uri.URI, ast doc := ast if f != file { - pf, err := ss.Parse(ctx, f) + pf, err := view.Parse(ctx, f) if err != nil || pf.AST() == nil { return } diff --git a/lsp/source/workspace.go b/lsp/source/workspace.go index 4e28e40..566180c 100644 --- a/lsp/source/workspace.go +++ b/lsp/source/workspace.go @@ -15,12 +15,12 @@ import ( // URI, symbols in source order. An empty query matches everything; the // result is capped at maxResults (0 means unlimited). Matching is // case-insensitive substring on the symbol name. -func WorkspaceSymbols(ctx context.Context, ss *cache.Snapshot, files []uri.URI, query string, maxResults int) []protocol.SymbolInformation { +func WorkspaceSymbols(ctx context.Context, view *cache.View, files []uri.URI, query string, maxResults int) []protocol.SymbolInformation { res := make([]protocol.SymbolInformation, 0, 64) q := strings.ToLower(query) for _, file := range files { - for _, sym := range documentSymbolsFlat(ctx, ss, file) { + for _, sym := range documentSymbolsFlat(ctx, view, file) { if q != "" && !strings.Contains(strings.ToLower(sym.Name), q) { continue } @@ -38,10 +38,10 @@ func WorkspaceSymbols(ctx context.Context, ss *cache.Snapshot, files []uri.URI, // documentSymbolsFlat returns the document symbols of a file flattened // into workspace symbols: each child carries its parent's name as the // container, and the location points at the symbol's name. -func documentSymbolsFlat(ctx context.Context, ss *cache.Snapshot, file uri.URI) []protocol.SymbolInformation { +func documentSymbolsFlat(ctx context.Context, view *cache.View, file uri.URI) []protocol.SymbolInformation { syms := make([]protocol.SymbolInformation, 0, 16) - for _, sym := range DocumentSymbols(ctx, ss, file) { + for _, sym := range DocumentSymbols(ctx, view, file) { flattenSymbol(sym, file, "", &syms) } diff --git a/lsp/source/workspace_test.go b/lsp/source/workspace_test.go index 2f73125..46ccbef 100644 --- a/lsp/source/workspace_test.go +++ b/lsp/source/workspace_test.go @@ -71,11 +71,7 @@ func allWorkspaceSymbols(ctx context.Context, session *cache.Session, query stri sort.Slice(views, func(i, j int) bool { return views[i].Folder() < views[j].Folder() }) for _, view := range views { - ss, release := view.Snapshot() - - syms := WorkspaceSymbols(ctx, ss, view.KnownFiles(), query, maxResults-len(res)) - - release() + syms := WorkspaceSymbols(ctx, view, view.KnownFiles(), query, maxResults-len(res)) res = append(res, syms...) if maxResults > 0 && len(res) >= maxResults { diff --git a/lsp/stream_test.go b/lsp/stream_test.go index b75012e..0cca0a7 100644 --- a/lsp/stream_test.go +++ b/lsp/stream_test.go @@ -243,11 +243,11 @@ func (h *clientHarness) waitForMethod(t *testing.T, method string) rpcFrame { func TestServeStream(t *testing.T) { clientConn, serverConn := net.Pipe() - ss := NewStreamServer(&Options{}) + view := NewStreamServer(&Options{}) errCh := make(chan error, 1) go func() { - errCh <- ss.ServeStream(context.Background(), jsonrpc2.NewConn(jsonrpc2.NewStream(serverConn))) + errCh <- view.ServeStream(context.Background(), jsonrpc2.NewConn(jsonrpc2.NewStream(serverConn))) }() client := newClientHarness(clientConn) diff --git a/lsp/symbols.go b/lsp/symbols.go index 588d866..4a858fa 100644 --- a/lsp/symbols.go +++ b/lsp/symbols.go @@ -10,8 +10,8 @@ import ( ) func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) (result protocol.DocumentSymbolSlice, err error) { - return withSnapshot(s.session, params.TextDocument.URI, func(ss *cache.Snapshot) (protocol.DocumentSymbolSlice, error) { - syms := source.DocumentSymbols(ctx, ss, params.TextDocument.URI) + return withView(s.session, params.TextDocument.URI, func(view *cache.View) (protocol.DocumentSymbolSlice, error) { + syms := source.DocumentSymbols(ctx, view, params.TextDocument.URI) result := make(protocol.DocumentSymbolSlice, 0, len(syms)) for i := range syms { diff --git a/main.go b/main.go index abc26b1..d2f8eb7 100644 --- a/main.go +++ b/main.go @@ -389,10 +389,7 @@ func checkFiles(ctx context.Context, files []string, folder string, includePaths return nil, err } - ss, release := v.Snapshot() - - res, err := source.NewDiagnostic().Diagnostic(ctx, ss, uris) - release() + res, err := source.NewDiagnostic().Diagnostic(ctx, v, uris) if err != nil { return nil, err }