diff --git a/check/check.go b/check/check.go index 15d26a3..76a79fc 100644 --- a/check/check.go +++ b/check/check.go @@ -12,10 +12,11 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // Request is one check run over materialized files. @@ -87,7 +88,7 @@ type Result struct { Fix *FixSummary } -// Run checks req.Files through the same cache and checker pipeline the LSP +// Run checks req.Files through the same store and checker pipeline the LSP // uses, and returns the diagnostics per file. It exits no process and // prints nothing; severity gating (failing CI on errors) belongs to the // caller. @@ -180,8 +181,8 @@ func runFix(ctx context.Context, files []string, folder string, includePaths []s version++ - if err := sess.Update(ctx, []*cache.FileChange{ - {URI: u, Version: version, Content: content, From: cache.FileChangeTypeDidChange}, + if err := sess.Update(ctx, []*vfs.FileChange{ + {URI: u, Version: version, Content: content, From: vfs.FileChangeTypeDidChange}, }); err != nil { return err } @@ -240,7 +241,7 @@ func runFix(ctx context.Context, files []string, folder string, includePaths []s // toDiagnostics translates one file's pipeline findings into printable // diagnostics: spans map through the file's mapper to 1-based UTF-16 // columns. -func toDiagnostics(ctx context.Context, view *cache.View, file uri.URI, diags []sema.Diagnostic) ([]Diagnostic, error) { +func toDiagnostics(ctx context.Context, view *store.View, file uri.URI, diags []sema.Diagnostic) ([]Diagnostic, error) { pf, err := view.Parse(ctx, file) if err != nil { return nil, err @@ -267,7 +268,7 @@ func toDiagnostics(ctx context.Context, view *cache.View, file uri.URI, diags [] // position maps a parser offset to 1-based file coordinates with UTF-16 // columns. When the offset does not map, the parser's own 1-based // coordinates pass through. -func position(pf *cache.ParsedFile, pos syntax.Position) (line, col int) { +func position(pf *store.ParsedFile, pos syntax.Position) (line, col int) { p, err := pf.Mapper().OffsetToLSPPosition(pos.Offset) if err != nil { return pos.Line, pos.Col @@ -295,11 +296,11 @@ func toSeverity(s sema.Severity) Severity { // openSession opens a session with the files open in the overlay of // a view rooted at folder, and returns the session (needed to push new // content into the overlay later), its view, and the files' URIs. -func openSession(ctx context.Context, files []string, folder string, includePaths []string) (*cache.Session, *cache.View, []uri.URI, error) { - sess := cache.NewSession(cache.NewMemoizedFS()) +func openSession(ctx context.Context, files []string, folder string, includePaths []string) (*store.Session, *store.View, []uri.URI, error) { + sess := store.NewSession(vfs.NewMemoizedFS()) view := sess.AddView(uri.File(folder), includePaths) - changes := make([]*cache.FileChange, 0, len(files)) + changes := make([]*vfs.FileChange, 0, len(files)) uris := make([]uri.URI, 0, len(files)) for _, file := range files { @@ -310,7 +311,7 @@ func openSession(ctx context.Context, files []string, folder string, includePath u := uri.File(file) uris = append(uris, u) - changes = append(changes, &cache.FileChange{URI: u, Version: 0, Content: content, From: cache.FileChangeTypeDidOpen}) + changes = append(changes, &vfs.FileChange{URI: u, Version: 0, Content: content, From: vfs.FileChangeTypeDidOpen}) } if err := sess.Update(ctx, changes); err != nil { diff --git a/lsp/cache/compat.go b/lsp/cache/compat.go deleted file mode 100644 index ab51c77..0000000 --- a/lsp/cache/compat.go +++ /dev/null @@ -1,34 +0,0 @@ -package cache - -// Temporary aliases for the file sources extracted to vfs. They keep the -// tree green while the document store moves to store; step 2 deletes this -// file and rewrites importers to vfs directly. -import "github.com/karitham/thrift-ls/vfs" - -type ( - FileSource = vfs.FileSource - FileHandle = vfs.FileHandle - Checker = vfs.Checker - FileID = vfs.FileID - DiskFile = vfs.DiskFile - Overlay = vfs.Overlay - OverlayFS = vfs.OverlayFS - overlayFS = vfs.OverlayFS - FileChange = vfs.FileChange - FileChangeType = vfs.FileChangeType -) - -const ( - FileChangeTypeInitialize = vfs.FileChangeTypeInitialize - FileChangeTypeDidOpen = vfs.FileChangeTypeDidOpen - FileChangeTypeDidChange = vfs.FileChangeTypeDidChange - FileChangeTypeDidClose = vfs.FileChangeTypeDidClose -) - -var ( - NewMemFS = vfs.NewMemFS - NewMemoizedFS = vfs.NewMemoizedFS - NewOverlayFS = vfs.NewOverlayFS - NewOverlay = vfs.NewOverlay - GetFileID = vfs.GetFileID -) diff --git a/lsp/codeaction.go b/lsp/codeaction.go index f08788b..a3bb999 100644 --- a/lsp/codeaction.go +++ b/lsp/codeaction.go @@ -7,8 +7,9 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // codeAction returns the code actions for the document: quickfixes for the @@ -17,7 +18,7 @@ import ( // fixes a reported diagnostic is 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.viewOf, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.CommandOrCodeAction, error) { + return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *store.View, fh vfs.FileHandle) ([]protocol.CommandOrCodeAction, error) { pf, err := view.Parse(ctx, params.TextDocument.URI) if err != nil { return nil, err @@ -51,7 +52,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara // toSemaSpan converts an LSP range to the pipeline's parser-coordinate // span through the file's mapper. -func toSemaSpan(pf *cache.ParsedFile, rng protocol.Range) (sema.Span, error) { +func toSemaSpan(pf *store.ParsedFile, rng protocol.Range) (sema.Span, error) { m := pf.Mapper() start, err := m.LSPPosToParserPosition(rng.Start) @@ -68,7 +69,7 @@ func toSemaSpan(pf *cache.ParsedFile, rng protocol.Range) (sema.Span, error) { } // toProtocolCodeAction translates a pipeline action to the wire type. -func toProtocolCodeAction(pf *cache.ParsedFile, a sema.Action) protocol.CodeAction { +func toProtocolCodeAction(pf *store.ParsedFile, a sema.Action) protocol.CodeAction { kind := protocol.CodeActionKindRefactorRewrite if a.Fix { kind = protocol.CodeActionKindQuickFix diff --git a/lsp/codejump.go b/lsp/codejump.go index 9d4e4e9..46c0209 100644 --- a/lsp/codejump.go +++ b/lsp/codejump.go @@ -5,24 +5,24 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) (result []protocol.Location, err error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) ([]protocol.Location, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) ([]protocol.Location, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) ([]protocol.Location, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.View) ([]protocol.Location, error) { return source.TypeDefinition(ctx, view, params.TextDocument.URI, params.Position) }) } diff --git a/lsp/configsource.go b/lsp/configsource.go index 0b7f3ce..24ec4a0 100644 --- a/lsp/configsource.go +++ b/lsp/configsource.go @@ -10,8 +10,8 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/options" + "github.com/karitham/thrift-ls/vfs" ) // FileConfigSource discovers thrift-ls.json through src: THRIFT_LS_CONFIG @@ -24,7 +24,7 @@ import ( // Reads use context.Background: resolution runs at view creation, outside // any request scope. A missing candidate simply continues the walk; any // other read or parse failure aborts with the candidate path attached. -func FileConfigSource(src cache.FileSource) ConfigSource { +func FileConfigSource(src vfs.FileSource) ConfigSource { return func(dir string) (options.Resolved, error) { if env := os.Getenv("THRIFT_LS_CONFIG"); env != "" { abs, err := filepath.Abs(env) @@ -64,7 +64,7 @@ func FileConfigSource(src cache.FileSource) ConfigSource { // readConfigDocument reads and parses one candidate config file through src. // found is false when the file does not exist; any other failure carries // the candidate path for diagnostics. -func readConfigDocument(src cache.FileSource, path string) (options.Resolved, bool, error) { +func readConfigDocument(src vfs.FileSource, path string) (options.Resolved, bool, error) { fh, err := src.ReadFile(context.Background(), uri.File(path)) if err != nil { if errors.Is(err, fs.ErrNotExist) { diff --git a/lsp/diagnostic.go b/lsp/diagnostic.go index 68b9a63..d336f80 100644 --- a/lsp/diagnostic.go +++ b/lsp/diagnostic.go @@ -9,16 +9,16 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" ) // lintConfig returns the analysis pipeline config for a view's folder: // the folder's thrift-ls.json lint settings overlaid by the workspace // settings. New severities apply from the next analysis run. -func (s *Server) lintConfig(view *cache.View) sema.Config { +func (s *Server) lintConfig(view *store.View) sema.Config { s.optsMu.RLock() overlay := s.workspaceOverlay s.optsMu.RUnlock() @@ -49,7 +49,7 @@ func lintConfigOf(l options.LintConfig) sema.Config { return sema.ConfigFromLint(disabled, severity) } -func (s *Server) pipeline(view *cache.View) *sema.Pipeline { +func (s *Server) pipeline(view *store.View) *sema.Pipeline { return sema.DefaultPipeline(s.lintConfig(view)). WithAnalyzers(s.analysis.Analyzers...). WithFixers(s.analysis.Fixers...). @@ -59,14 +59,14 @@ func (s *Server) pipeline(view *cache.View) *sema.Pipeline { // diagnose runs the analysis pipeline once over every affected file — one // run, one shared cross-file index — and publishes the findings per file. // The per-file findings are cached for code actions. -func (s *Server) diagnose(ctx context.Context, view *cache.View, affected []uri.URI) { +func (s *Server) diagnose(ctx context.Context, view *store.View, affected []uri.URI) { s.diagnoseAt(ctx, view, affected, view.Generation()) } -func (s *Server) diagnoseAt(ctx context.Context, view *cache.View, affected []uri.URI, generation uint64) { +func (s *Server) diagnoseAt(ctx context.Context, view *store.View, affected []uri.URI, generation uint64) { s.analysisMu.Lock() defer s.analysisMu.Unlock() - ctx = cache.WithGeneration(ctx, generation) + ctx = store.WithGeneration(ctx, generation) slog.Debug("diagnose called", "files", len(affected)) defer slog.Debug("diagnose finished") diff --git a/lsp/filechange.go b/lsp/filechange.go index 4b0bf28..69fa256 100644 --- a/lsp/filechange.go +++ b/lsp/filechange.go @@ -3,26 +3,26 @@ package lsp import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) // FileChangeFromLSPDidChange converts a didChange payload into file // changes. The server advertises whole-document sync, so whole-document // events map one-to-one; incremental (partial) events are a client // protocol violation and are skipped. -func FileChangeFromLSPDidChange(params *protocol.DidChangeTextDocumentParams) []*cache.FileChange { - changes := make([]*cache.FileChange, 0, len(params.ContentChanges)) +func FileChangeFromLSPDidChange(params *protocol.DidChangeTextDocumentParams) []*vfs.FileChange { + changes := make([]*vfs.FileChange, 0, len(params.ContentChanges)) for i := range params.ContentChanges { event, ok := params.ContentChanges[i].(*protocol.TextDocumentContentChangeWholeDocument) if !ok { continue } - changes = append(changes, &cache.FileChange{ + changes = append(changes, &vfs.FileChange{ URI: params.TextDocument.URI, Version: int(params.TextDocument.Version), Content: []byte(event.Text), - From: cache.FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }) } diff --git a/lsp/filerename.go b/lsp/filerename.go index ca397e5..b50f99a 100644 --- a/lsp/filerename.go +++ b/lsp/filerename.go @@ -7,8 +7,9 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // willRenameFiles computes the edits re-pointing every include of each @@ -70,8 +71,8 @@ func (s *Server) didRenameFiles(ctx context.Context, params *protocol.RenameFile // Forget the editor overlay first, mirroring didClose: otherwise // the stale buffer shadows the file system forever, and the // watcher guard drops this rename's delete/create events. - change := &cache.FileChange{URI: oldURI, From: cache.FileChangeTypeDidClose} - if err := s.session.Update(ctx, []*cache.FileChange{change}); err != nil { + change := &vfs.FileChange{URI: oldURI, From: vfs.FileChangeTypeDidClose} + if err := s.session.Update(ctx, []*vfs.FileChange{change}); err != nil { return err } @@ -87,8 +88,8 @@ func (s *Server) didRenameFiles(ctx context.Context, params *protocol.RenameFile // store, so their include edges re-resolve after a rename even in clients // without working file watchers. Overlay-backed includers are re-fed their // own current content: a cheap no-op re-parse. -func (s *Server) refreshIncluders(ctx context.Context, view *cache.View, oldURI uri.URI) { - var refreshes []*cache.FileChange +func (s *Server) refreshIncluders(ctx context.Context, view *store.View, oldURI uri.URI) { + var refreshes []*vfs.FileChange for _, inc := range view.Includers(oldURI) { fh, err := s.session.ReadFile(ctx, inc) @@ -105,11 +106,11 @@ func (s *Server) refreshIncluders(ctx context.Context, view *cache.View, oldURI continue } - refreshes = append(refreshes, &cache.FileChange{ + refreshes = append(refreshes, &vfs.FileChange{ URI: inc, Version: int(fh.Version()), Content: content, - From: cache.FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }) } diff --git a/lsp/folding.go b/lsp/folding.go index 25a1200..ecd9678 100644 --- a/lsp/folding.go +++ b/lsp/folding.go @@ -5,12 +5,12 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) foldingRanges(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) ([]protocol.FoldingRange, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.View) ([]protocol.FoldingRange, error) { return source.Ranges(ctx, view, params.TextDocument.URI), nil }) } diff --git a/lsp/format.go b/lsp/format.go index 8054be3..59de03f 100644 --- a/lsp/format.go +++ b/lsp/format.go @@ -5,12 +5,13 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) (result []protocol.TextEdit, err error) { - return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.TextEdit, error) { + return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *store.View, fh vfs.FileHandle) ([]protocol.TextEdit, error) { edit, err := source.FormatDocument(ctx, view, fh, s.formatOptions(view)) if err != nil { return nil, err @@ -25,7 +26,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.viewOf, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.TextEdit, error) { + return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *store.View, fh vfs.FileHandle) ([]protocol.TextEdit, error) { return source.FormatRange(ctx, view, fh, s.formatOptions(view), params.Range) }) } diff --git a/lsp/frontend.go b/lsp/frontend.go index 03fc2c0..6928839 100644 --- a/lsp/frontend.go +++ b/lsp/frontend.go @@ -1,9 +1,9 @@ package lsp import ( - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/vfs" ) // Layering, lowest to highest precedence: @@ -45,7 +45,7 @@ type Analysis struct { type Options struct { // Files serves file content and directory walks. Nil uses the memoized // disk source. - Files cache.FileSource + Files vfs.FileSource // Defaults sits directly above the builtin defaults. An empty patch // uses the package defaults. Defaults options.Patch diff --git a/lsp/harness_test.go b/lsp/harness_test.go index 4beeaca..a39eee4 100644 --- a/lsp/harness_test.go +++ b/lsp/harness_test.go @@ -12,7 +12,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // testClient records everything the server pushes: diagnostics per URI, @@ -151,7 +152,7 @@ func symbolNames(syms protocol.SymbolInformationSlice) []string { // newTestServer returns an in-memory server with synchronous diagnostics. func newTestServer(client protocol.Client) *Server { - srv := NewServer(client, Options{Files: cache.NewMemFS(nil)}) + srv := NewServer(client, Options{Files: vfs.NewMemFS(nil)}) srv.diagSync = true return srv @@ -159,7 +160,7 @@ func newTestServer(client protocol.Client) *Server { // newMemServer returns an in-memory server seeded with files. func newMemServer(files map[uri.URI][]byte) *Server { - srv := NewServer(nil, Options{Files: cache.NewMemFS(files)}) + srv := NewServer(nil, Options{Files: vfs.NewMemFS(files)}) srv.diagSync = true return srv @@ -168,7 +169,7 @@ func newMemServer(files map[uri.URI][]byte) *Server { // newSyncServerWithOptions builds a synchronous server over a MemFS seed. // opts.Files is always the MemFS; other fields carry through. func newSyncServerWithOptions(client protocol.Client, files map[uri.URI][]byte, opts Options) *Server { - opts.Files = cache.NewMemFS(files) + opts.Files = vfs.NewMemFS(files) srv := NewServer(client, opts) srv.diagSync = true @@ -313,7 +314,7 @@ func formatText(t *testing.T, srv *Server, file uri.URI) string { func diagnosePair(t *testing.T, srv *Server, file uri.URI) { t.Helper() - _, err := withFile(t.Context(), srv.session.ViewOf, file, func(view *cache.View, _ cache.FileHandle) (struct{}, error) { + _, err := withFile(t.Context(), srv.session.ViewOf, file, func(view *store.View, _ vfs.FileHandle) (struct{}, error) { srv.diagnose(t.Context(), view, []uri.URI{file}) return struct{}{}, nil diff --git a/lsp/highlight.go b/lsp/highlight.go index 577db63..7a1f5c3 100644 --- a/lsp/highlight.go +++ b/lsp/highlight.go @@ -5,12 +5,12 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) ([]protocol.DocumentHighlight, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 a51df5f..aa29494 100644 --- a/lsp/hover.go +++ b/lsp/hover.go @@ -6,12 +6,12 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) (*protocol.Hover, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 903341d..3df52df 100644 --- a/lsp/impl.go +++ b/lsp/impl.go @@ -12,8 +12,9 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { @@ -23,11 +24,11 @@ func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocume } fileURI := document.URI - change := &cache.FileChange{ + change := &vfs.FileChange{ URI: fileURI, Version: int(document.Version), Content: []byte(document.Text), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, } // Opening a file no project owns loads its directory as a folder, so @@ -39,7 +40,7 @@ func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocume s.workspace.loadSync(ctx, []uri.URI{uri.File(filepath.Dir(fileURI.FsPath()))}) } - return s.applyChanges(ctx, []*cache.FileChange{change}, true) + return s.applyChanges(ctx, []*vfs.FileChange{change}, true) } func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { @@ -48,21 +49,21 @@ func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDo func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { fileURI := params.TextDocument.URI - change := &cache.FileChange{URI: fileURI, From: cache.FileChangeTypeDidClose} + change := &vfs.FileChange{URI: fileURI, From: vfs.FileChangeTypeDidClose} - return s.applyChanges(ctx, []*cache.FileChange{change}, true) + return s.applyChanges(ctx, []*vfs.FileChange{change}, true) } // applyChanges is the single path from file events to overlays and views. // Files route only through snapshot ownership: unowned files update the // overlay but reach no view. -func (s *Server) applyChanges(ctx context.Context, changes []*cache.FileChange, overlay bool) error { +func (s *Server) applyChanges(ctx context.Context, changes []*vfs.FileChange, overlay bool) error { if len(changes) == 0 { return nil } for _, change := range changes { - if change.From == cache.FileChangeTypeDidClose { + if change.From == vfs.FileChangeTypeDidClose { s.forgetReport(change.URI) } } @@ -71,7 +72,7 @@ func (s *Server) applyChanges(ctx context.Context, changes []*cache.FileChange, } func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { - var changes []*cache.FileChange + var changes []*vfs.FileChange for _, event := range params.Changes { if !s.workspace.owns(event.URI) { @@ -98,9 +99,9 @@ func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.Did // watchedFileChange builds a FileChange from a disk event, reading the // current content through the memoized file source. Deleted files are // reported as a close change. -func (s *Server) watchedFileChange(ctx context.Context, event protocol.FileEvent) (*cache.FileChange, error) { +func (s *Server) watchedFileChange(ctx context.Context, event protocol.FileEvent) (*vfs.FileChange, error) { if event.Type == protocol.FileChangeTypeDeleted { - return &cache.FileChange{URI: event.URI, From: cache.FileChangeTypeDidClose}, nil + return &vfs.FileChange{URI: event.URI, From: vfs.FileChangeTypeDidClose}, nil } fh, err := s.session.ReadFile(ctx, event.URI) @@ -113,11 +114,11 @@ func (s *Server) watchedFileChange(ctx context.Context, event protocol.FileEvent return nil, err } - return &cache.FileChange{ + return &vfs.FileChange{ URI: event.URI, Version: int(fh.Version()), Content: content, - From: cache.FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }, nil } @@ -126,7 +127,7 @@ func (s *Server) watchedFileChange(ctx context.Context, event protocol.FileEvent // editor request thread. 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, res cache.ChangeResult) { +func (s *Server) postDiagnostics(ctx context.Context, view *store.View, res store.ChangeResult) { if s.diagSync { s.diagnoseAt(context.WithoutCancel(ctx), view, res.Affected, res.Gen) @@ -157,7 +158,7 @@ func (s *Server) postDiagnostics(ctx context.Context, view *cache.View, res cach } func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { - return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) (*protocol.CompletionList, error) { + return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *store.View, fh vfs.FileHandle) (*protocol.CompletionList, error) { items, rng, truncated, err := source.DefaultTokenCompletion.Completion(ctx, view, &source.CompletionRequest{ Pos: protocol.Position{ Line: params.Position.Line, diff --git a/lsp/links.go b/lsp/links.go index 5afc024..08abb4b 100644 --- a/lsp/links.go +++ b/lsp/links.go @@ -5,12 +5,12 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) ([]protocol.DocumentLink, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.View) ([]protocol.DocumentLink, error) { return source.Links(ctx, view, params.TextDocument.URI), nil }) } diff --git a/lsp/protocol_test.go b/lsp/protocol_test.go index 3e2dd63..2c60f69 100644 --- a/lsp/protocol_test.go +++ b/lsp/protocol_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) func TestInitializeReportsConfiguredVersion(t *testing.T) { @@ -19,7 +19,7 @@ func TestInitializeReportsConfiguredVersion(t *testing.T) { {name: "fallback", want: ServerVersion}, } { t.Run(tt.name, func(t *testing.T) { - srv := NewServer(nil, Options{Files: cache.NewMemFS(nil), Version: tt.version}) + srv := NewServer(nil, Options{Files: vfs.NewMemFS(nil), Version: tt.version}) result, err := srv.Initialize(t.Context(), testInitializeParams(nil)) require.NoError(t, err) version, ok := result.ServerInfo.Version.Get() diff --git a/lsp/rename.go b/lsp/rename.go index 5de869f..b4878d5 100644 --- a/lsp/rename.go +++ b/lsp/rename.go @@ -5,18 +5,18 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.Range, error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) (*protocol.Range, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) (*protocol.WorkspaceEdit, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 99ccaa6..5ebd7f4 100644 --- a/lsp/semantic.go +++ b/lsp/semantic.go @@ -5,12 +5,12 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) semanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) (*protocol.SemanticTokens, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.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 e642a56..f6ed602 100644 --- a/lsp/server.go +++ b/lsp/server.go @@ -13,14 +13,15 @@ import ( "go.lsp.dev/uri" "github.com/karitham/thrift-ls/formatter" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) type Server struct { - session *cache.Session + session *store.Session client protocol.Client @@ -83,7 +84,7 @@ type Server struct { func NewServer(client protocol.Client, opts Options) *Server { fs := opts.Files if fs == nil { - fs = cache.NewMemoizedFS() + fs = vfs.NewMemoizedFS() } configSource := opts.ConfigSource if configSource == nil { @@ -96,7 +97,7 @@ func NewServer(client protocol.Client, opts Options) *Server { defaults := opts.Defaults.Apply(options.Default()) server := &Server{ - session: cache.NewSession(fs), + session: store.NewSession(fs), client: client, defaults: defaults, configSource: configSource, @@ -142,7 +143,7 @@ func (s *Server) setWorkspaceSettings(overlay options.Patch) { slog.Debug("workspace settings applied") } -func (s *Server) addProjectView(project Project) *cache.View { +func (s *Server) addProjectView(project Project) *store.View { cfg := s.projectViewConfig(project) return s.addView(project.RootURI, cfg, derefIncludePaths(cfg.IncludePaths)) } @@ -154,7 +155,7 @@ func derefIncludePaths(p *[]string) []string { return *p } -func (s *Server) addView(folder uri.URI, cfg options.Patch, includePaths []string) *cache.View { +func (s *Server) addView(folder uri.URI, cfg options.Patch, includePaths []string) *store.View { s.applyLogLevel(cfg) s.cfgMu.Lock() @@ -329,7 +330,7 @@ func (s *Server) applyLogLevel(cfg options.Patch) { // formatOptions returns the folder's config with the workspace settings // overlay applied. -func (s *Server) formatOptions(view *cache.View) formatter.Options { +func (s *Server) formatOptions(view *store.View) formatter.Options { s.optsMu.RLock() overlay := s.workspaceOverlay s.optsMu.RUnlock() @@ -552,7 +553,7 @@ 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.viewOf, params.TextDocument.URI, func(view *cache.View, fh cache.FileHandle) ([]protocol.TextEdit, error) { + return withFile(ctx, s.viewOf, params.TextDocument.URI, func(view *store.View, fh vfs.FileHandle) ([]protocol.TextEdit, error) { return source.OnTypeFormat(ctx, view, fh, s.formatOptions(view), params.Position) }) } @@ -588,7 +589,7 @@ func (s *Server) SignatureHelp(ctx context.Context, params *protocol.SignatureHe func (s *Server) Symbols(ctx context.Context, params *protocol.WorkspaceSymbolParams) (result protocol.WorkspaceSymbolResult, err error) { views := s.session.Views() - slices.SortFunc(views, func(a, b *cache.View) int { + slices.SortFunc(views, func(a, b *store.View) int { return strings.Compare(string(a.Folder()), string(b.Folder())) }) diff --git a/lsp/snapshot.go b/lsp/snapshot.go index aa900ca..b00b4ed 100644 --- a/lsp/snapshot.go +++ b/lsp/snapshot.go @@ -5,14 +5,15 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) -type viewResolver func(uri.URI) (*cache.View, error) +type viewResolver func(uri.URI) (*store.View, 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](resolve viewResolver, file uri.URI, fn func(*cache.View) (T, error)) (T, error) { +func withView[T any](resolve viewResolver, file uri.URI, fn func(*store.View) (T, error)) (T, error) { view, err := resolve(file) if err != nil { var zero T @@ -24,8 +25,8 @@ func withView[T any](resolve viewResolver, file uri.URI, fn func(*cache.View) (T } // withFile is withView plus the file handle for file. -func withFile[T any](ctx context.Context, resolve viewResolver, file uri.URI, fn func(*cache.View, cache.FileHandle) (T, error)) (T, error) { - return withView(resolve, file, func(view *cache.View) (T, error) { +func withFile[T any](ctx context.Context, resolve viewResolver, file uri.URI, fn func(*store.View, vfs.FileHandle) (T, error)) (T, error) { + return withView(resolve, file, func(view *store.View) (T, error) { fh, err := view.ReadFile(ctx, file) if err != nil { var zero T @@ -37,6 +38,6 @@ func withFile[T any](ctx context.Context, resolve viewResolver, file uri.URI, fn }) } -func (s *Server) viewOf(file uri.URI) (*cache.View, error) { +func (s *Server) viewOf(file uri.URI) (*store.View, error) { return s.workspace.viewOf(file) } diff --git a/lsp/source/annotation_navigation_test.go b/lsp/source/annotation_navigation_test.go index f014daf..2901838 100644 --- a/lsp/source/annotation_navigation_test.go +++ b/lsp/source/annotation_navigation_test.go @@ -10,7 +10,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // Test_StructuredAnnotationNavigation covers the cursor features on @@ -29,12 +30,12 @@ struct S { } ` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/anno.thrift", Version: 0, Content: []byte(content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -49,7 +50,7 @@ struct S { t.Run("locations", func(t *testing.T) { tests := []struct { name string - probe func(context.Context, *cache.View, uri.URI, protocol.Position) ([]protocol.Location, error) + probe func(context.Context, *store.View, uri.URI, protocol.Position) ([]protocol.Location, error) pos protocol.Position want []protocol.Location }{ diff --git a/lsp/source/completion_qualified_type_test.go b/lsp/source/completion_qualified_type_test.go index fa98e94..ca4e70d 100644 --- a/lsp/source/completion_qualified_type_test.go +++ b/lsp/source/completion_qualified_type_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) // TestCompletionQualifiedType covers "songs.|" in type positions: the dot @@ -48,8 +48,8 @@ const i32 TEMPO = 120` for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { 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}, + &vfs.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte(songs), From: vfs.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(tt.content), From: vfs.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, tt.content, tt.marker) @@ -73,8 +73,8 @@ func TestCompletionQualifiedTypeFilter(t *testing.T) { content := "include \"songs.thrift\"\nstruct Club {\n\t1: required songs.A\n}" 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}, + &vfs.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: vfs.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(content), From: vfs.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, content, "songs.A") diff --git a/lsp/source/completion_test.go b/lsp/source/completion_test.go index 064d4eb..cc013f2 100644 --- a/lsp/source/completion_test.go +++ b/lsp/source/completion_test.go @@ -8,18 +8,19 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // buildSnapshot builds a view from file contents with optional include // paths. -func buildSnapshot(t *testing.T, includePaths []string, files ...*cache.FileChange) *cache.View { +func buildSnapshot(t *testing.T, includePaths []string, files ...*vfs.FileChange) *store.View { t.Helper() - c := cache.NewMemoizedFS() - fs := cache.NewOverlayFS(c) + c := vfs.NewMemoizedFS() + fs := vfs.NewOverlayFS(c) _ = fs.Update(t.Context(), files) - view := cache.NewView(uri.File("/tmp"), fs, includePaths) + view := store.NewView(uri.File("/tmp"), fs, includePaths) for _, f := range files { _, _ = view.Parse(t.Context(), f.URI) @@ -31,7 +32,7 @@ func buildSnapshot(t *testing.T, includePaths []string, files ...*cache.FileChan func TestCompletionEndToEnd(t *testing.T) { content := "struct User {\n 1: required i64 id\n}\n\nstruct Profile {\n 1: required Us\n}" view := buildSnapshot(t, nil, - &cache.FileChange{URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(content), From: vfs.FileChangeTypeDidOpen}, ) fh, err := view.ReadFile(t.Context(), "file:///tmp/test.thrift") diff --git a/lsp/source/completion_type_slot_test.go b/lsp/source/completion_type_slot_test.go index 151b24b..aad3a18 100644 --- a/lsp/source/completion_type_slot_test.go +++ b/lsp/source/completion_type_slot_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) // TestCompletionTypeSlotNoLeaks pins that type slots only suggest types: @@ -40,8 +40,8 @@ struct Album { content := "include \"songs.thrift\"\nstruct Club {\n\t" + tt.marker + "\n}" 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}, + &vfs.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte(inc), From: vfs.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(content), From: vfs.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, content, tt.marker) @@ -63,8 +63,8 @@ func TestCompletionImportedTypesQualified(t *testing.T) { content := "include \"songs.thrift\"\nstruct Club {\n\t1: required so\n}" 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}, + &vfs.FileChange{URI: "file:///tmp/songs.thrift", Version: 0, Content: []byte("struct Album {\n\t1: required string title\n}\n"), From: vfs.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/club.thrift", Version: 0, Content: []byte(content), From: vfs.FileChangeTypeDidOpen}, ) pos := lspPosOf(t, content, "required so") diff --git a/lsp/source/cross_project_test.go b/lsp/source/cross_project_test.go index 99048c0..0d53484 100644 --- a/lsp/source/cross_project_test.go +++ b/lsp/source/cross_project_test.go @@ -9,7 +9,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // TestDefinitionCrossProjectInclude verifies go-to-definition across project @@ -26,18 +27,18 @@ 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" - view := cache.BuildViewForTestWithPaths([]string{includeDir}, []*cache.FileChange{ + view := store.BuildViewForTestWithPaths([]string{includeDir}, []*vfs.FileChange{ { URI: gundamFile, Version: 0, Content: []byte("namespace * gundam\n\nstruct MobileSuit {\n 1: string model\n}"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: appFile, Version: 0, Content: []byte(appContent), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/lsp/source/cross_reference_test.go b/lsp/source/cross_reference_test.go index 17cdd13..5e242cf 100644 --- a/lsp/source/cross_reference_test.go +++ b/lsp/source/cross_reference_test.go @@ -7,7 +7,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // TestReferenceBareCrossFile covers find-references across an include when @@ -25,9 +26,9 @@ struct StrikeRouge { 1: required string Name }` - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(gundamFile), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainFile), From: vfs.FileChangeTypeDidOpen}, }) // Cursor on "Gundam" (the definition name) in federation.gundam.thrift. @@ -55,9 +56,9 @@ struct StrikeRouge { 1: required string Name }` - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(gundamFile), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainFile), From: vfs.FileChangeTypeDidOpen}, }) locations, err := Reference(t.Context(), view, "file:///tmp/federation.gundam.thrift", protocol.Position{Line: 0, Character: 7}) @@ -92,10 +93,10 @@ struct Gundam { 1: required string Model }` - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/mobile_suit.zeon.thrift", Version: 0, Content: []byte(zeonFile), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(federationFile), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainFile), From: vfs.FileChangeTypeDidOpen}, }) // Cursor on "Zaku" in main.thrift (line 3, character 13: the type of diff --git a/lsp/source/definition.go b/lsp/source/definition.go index 7a816b4..09ab715 100644 --- a/lsp/source/definition.go +++ b/lsp/source/definition.go @@ -6,14 +6,14 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // 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, view *cache.View, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { +func Definition(ctx context.Context, view *store.View, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) pf, target, err := resolveTarget(ctx, view, file, pos) @@ -33,7 +33,7 @@ func Definition(ctx context.Context, view *cache.View, file uri.URI, pos protoco return res, err } -func typeNameDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { +func typeNameDefinition(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) ([]protocol.Location, error) { ft := target.fieldType() if ft == nil { return nil, nil @@ -52,7 +52,7 @@ func typeNameDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFil return []protocol.Location{loc}, nil } -func constValueDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { +func constValueDefinition(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) ([]protocol.Location, error) { def, err := ix.ResolveValue(ctx, pf, target.node.(*syntax.ConstValue)) if err != nil || def == nil { return nil, err @@ -66,7 +66,7 @@ func constValueDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedF return []protocol.Location{loc}, nil } -func serviceDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { +func serviceDefinition(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) ([]protocol.Location, error) { def, err := ix.ResolveService(ctx, pf, target.identifier()) if err != nil || def == nil { return nil, err diff --git a/lsp/source/definition_list_test.go b/lsp/source/definition_list_test.go index dd0ae80..4ddcabb 100644 --- a/lsp/source/definition_list_test.go +++ b/lsp/source/definition_list_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestDefinition_EnumValueInConstList(t *testing.T) { @@ -23,18 +24,18 @@ func TestDefinition_EnumValueInConstList(t *testing.T) { const list my_list = [MyEnum.Value1, MyEnum.Value2]` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(file), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View file uri.URI pos protocol.Position } diff --git a/lsp/source/definition_test.go b/lsp/source/definition_test.go index aed3fbc..de19d5a 100644 --- a/lsp/source/definition_test.go +++ b/lsp/source/definition_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestDefinition(t *testing.T) { @@ -58,36 +59,36 @@ struct Person { 2: required user.Test field2, }` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/user.extra.thrift", Version: 0, Content: []byte(file3), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/app.thrift", Version: 0, Content: []byte(file4), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View file uri.URI pos protocol.Position } diff --git a/lsp/source/document.go b/lsp/source/document.go index ac502fe..d453310 100644 --- a/lsp/source/document.go +++ b/lsp/source/document.go @@ -6,12 +6,12 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // DocumentSymbols returns the document symbols of a file, in source order. -func DocumentSymbols(ctx context.Context, view *cache.View, file uri.URI) []*protocol.DocumentSymbol { +func DocumentSymbols(ctx context.Context, view *store.View, file uri.URI) []*protocol.DocumentSymbol { res := make([]*protocol.DocumentSymbol, 0) pf, err := view.Parse(ctx, file) @@ -37,7 +37,7 @@ func DocumentSymbols(ctx context.Context, view *cache.View, file uri.URI) []*pro //go:fix inline // nameRange returns the LSP range of an identifier. -func nameRange(pf *cache.ParsedFile, id *syntax.Identifier) protocol.Range { +func nameRange(pf *store.ParsedFile, id *syntax.Identifier) protocol.Range { if id == nil { return protocol.Range{} } @@ -48,7 +48,7 @@ func nameRange(pf *cache.ParsedFile, id *syntax.Identifier) protocol.Range { } // nodeSymbol builds the symbol for a top-level definition. -func nodeSymbol(pf *cache.ParsedFile, node syntax.Node) *protocol.DocumentSymbol { +func nodeSymbol(pf *store.ParsedFile, node syntax.Node) *protocol.DocumentSymbol { switch v := node.(type) { case *syntax.Typedef: return typedefSymbol(pf, v) @@ -72,7 +72,7 @@ func nodeSymbol(pf *cache.ParsedFile, node syntax.Node) *protocol.DocumentSymbol return nil } -func structSymbol(pf *cache.ParsedFile, st *syntax.Struct, detail string, kind protocol.SymbolKind) *protocol.DocumentSymbol { +func structSymbol(pf *store.ParsedFile, st *syntax.Struct, detail string, kind protocol.SymbolKind) *protocol.DocumentSymbol { res := &protocol.DocumentSymbol{ Name: st.Name.Text, Detail: &detail, @@ -90,7 +90,7 @@ func structSymbol(pf *cache.ParsedFile, st *syntax.Struct, detail string, kind p return res } -func enumSymbol(pf *cache.ParsedFile, enum *syntax.Enum) *protocol.DocumentSymbol { +func enumSymbol(pf *store.ParsedFile, enum *syntax.Enum) *protocol.DocumentSymbol { res := &protocol.DocumentSymbol{ Name: enum.Name.Text, Detail: new("Enum"), @@ -111,7 +111,7 @@ func enumSymbol(pf *cache.ParsedFile, enum *syntax.Enum) *protocol.DocumentSymbo return res } -func serviceSymbol(pf *cache.ParsedFile, svc *syntax.Service) *protocol.DocumentSymbol { +func serviceSymbol(pf *store.ParsedFile, svc *syntax.Service) *protocol.DocumentSymbol { res := &protocol.DocumentSymbol{ Name: svc.Name.Text, Kind: protocol.SymbolKindInterface, @@ -131,7 +131,7 @@ func serviceSymbol(pf *cache.ParsedFile, svc *syntax.Service) *protocol.Document return res } -func fieldSymbol(pf *cache.ParsedFile, field *syntax.Field) *protocol.DocumentSymbol { +func fieldSymbol(pf *store.ParsedFile, field *syntax.Field) *protocol.DocumentSymbol { return &protocol.DocumentSymbol{ Name: field.Name.Text, Kind: protocol.SymbolKindField, @@ -140,7 +140,7 @@ func fieldSymbol(pf *cache.ParsedFile, field *syntax.Field) *protocol.DocumentSy } } -func typedefSymbol(pf *cache.ParsedFile, td *syntax.Typedef) *protocol.DocumentSymbol { +func typedefSymbol(pf *store.ParsedFile, td *syntax.Typedef) *protocol.DocumentSymbol { return &protocol.DocumentSymbol{ Name: td.Name.Text, Detail: new("Typedef"), @@ -150,7 +150,7 @@ func typedefSymbol(pf *cache.ParsedFile, td *syntax.Typedef) *protocol.DocumentS } } -func constSymbol(pf *cache.ParsedFile, cst *syntax.Const) *protocol.DocumentSymbol { +func constSymbol(pf *store.ParsedFile, cst *syntax.Const) *protocol.DocumentSymbol { return &protocol.DocumentSymbol{ Name: cst.Name.Text, Detail: new("Const"), diff --git a/lsp/source/filerename.go b/lsp/source/filerename.go index 8d8cf84..e606bdf 100644 --- a/lsp/source/filerename.go +++ b/lsp/source/filerename.go @@ -8,14 +8,14 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" ) // RenameFileEdits returns, per including file, the edits retargeting every // include of oldURI at newURI's location. Only direct includers carry an // include literal naming the file; transitive dependents resolve through // them and need no edit. -func RenameFileEdits(ctx context.Context, view *cache.View, oldURI, newURI uri.URI) (map[uri.URI][]protocol.TextEdit, error) { +func RenameFileEdits(ctx context.Context, view *store.View, oldURI, newURI uri.URI) (map[uri.URI][]protocol.TextEdit, error) { res := make(map[uri.URI][]protocol.TextEdit) for _, f := range view.Includers(oldURI) { diff --git a/lsp/source/filerename_test.go b/lsp/source/filerename_test.go index 3185036..5f3ac9f 100644 --- a/lsp/source/filerename_test.go +++ b/lsp/source/filerename_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // litEdit is the expected edit of a whole include literal on one line. @@ -22,12 +23,12 @@ func litEdit(colStart, colEnd int, text string) protocol.TextEdit { } } -func openDoc(u, content string) *cache.FileChange { - return &cache.FileChange{ +func openDoc(u, content string) *vfs.FileChange { + return &vfs.FileChange{ URI: uri.URI(u), Version: 0, Content: []byte(content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, } } @@ -39,7 +40,7 @@ func openDoc(u, content string) *cache.FileChange { func Test_RenameFileEdits(t *testing.T) { tests := []struct { name string - files []*cache.FileChange + files []*vfs.FileChange paths []string // extra include paths, like --I oldURI string newURI string @@ -49,7 +50,7 @@ func Test_RenameFileEdits(t *testing.T) { }{ { name: "flat and relative includes both rewrite", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ openDoc("file:///tmp/ren/shared.thrift", "struct Shared {}\n"), openDoc("file:///tmp/ren/a.thrift", "include \"shared.thrift\"\nstruct A { 1: shared.X x }\n"), openDoc("file:///tmp/ren/sub/b.thrift", "include \"../shared.thrift\"\nstruct B { 1: shared.X x }\n"), @@ -65,7 +66,7 @@ func Test_RenameFileEdits(t *testing.T) { }, { name: "a file nobody includes yields no edits", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ openDoc("file:///tmp/solo/lonely.thrift", "struct Lonely {}\n"), }, oldURI: "file:///tmp/solo/lonely.thrift", @@ -74,7 +75,7 @@ func Test_RenameFileEdits(t *testing.T) { }, { name: "a move between directories under one include root rewrites the whole tail", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ openDoc("file:///tmp/inc/vendor/base.thrift", "struct Base {}\n"), openDoc("file:///tmp/proj/main.thrift", "include \"vendor/base.thrift\"\nstruct M { 1: vendor.B b }\n"), }, @@ -87,7 +88,7 @@ func Test_RenameFileEdits(t *testing.T) { }, { name: "an include resolved through an include path swaps only its base name", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ openDoc("file:///tmp/inc/vendor.thrift", "struct Vendor {}\n"), openDoc("file:///tmp/proj/main.thrift", "include \"vendor.thrift\"\nstruct M { 1: vendor.V v }\n"), }, @@ -100,7 +101,7 @@ func Test_RenameFileEdits(t *testing.T) { }, { name: "a self-include rewrites in place", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ openDoc("file:///tmp/self/loop.thrift", "include \"loop.thrift\"\nstruct L {}\n"), }, oldURI: "file:///tmp/self/loop.thrift", @@ -113,7 +114,7 @@ func Test_RenameFileEdits(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := cache.BuildViewForTestWithPaths(tt.paths, tt.files) + view := store.BuildViewForTestWithPaths(tt.paths, tt.files) got, err := RenameFileEdits(t.Context(), view, uri.URI(tt.oldURI), uri.URI(tt.newURI)) require.NoError(t, err) diff --git a/lsp/source/folding.go b/lsp/source/folding.go index d50f7f5..3b4dc75 100644 --- a/lsp/source/folding.go +++ b/lsp/source/folding.go @@ -12,13 +12,13 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // Ranges returns the folding ranges of a file, in source order. Degenerate // single-line ranges are omitted. -func Ranges(ctx context.Context, view *cache.View, file uri.URI) []protocol.FoldingRange { +func Ranges(ctx context.Context, view *store.View, file uri.URI) []protocol.FoldingRange { pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil @@ -116,7 +116,7 @@ func contentStart(n syntax.Node, toks []syntax.Token) int { // bracedRange returns the fold range of a brace-delimited body: from the // opening brace to the closing one. start is the first content token // (see contentStart). -func bracedRange(pf *cache.ParsedFile, n syntax.Node, start int) (protocol.FoldingRange, bool) { +func bracedRange(pf *store.ParsedFile, n syntax.Node, start int) (protocol.FoldingRange, bool) { open := -1 for i := start; i <= n.TokEnd(); i++ { @@ -178,7 +178,7 @@ func nodeAnnotation(n syntax.Node) *syntax.Annotations { // spanRange converts the token span [start, end] into a folding range. // Degenerate single-line spans yield no range. -func spanRange(pf *cache.ParsedFile, start, end int) (protocol.FoldingRange, bool) { +func spanRange(pf *store.ParsedFile, start, end int) (protocol.FoldingRange, bool) { doc := pf.AST() s := doc.TokenPosition(start) e := doc.TokenEndPosition(end) @@ -199,7 +199,7 @@ func spanRange(pf *cache.ParsedFile, start, end int) (protocol.FoldingRange, boo } // commentSpanRange is spanRange for comment folds. -func commentSpanRange(pf *cache.ParsedFile, start, end int) (protocol.FoldingRange, bool) { +func commentSpanRange(pf *store.ParsedFile, start, end int) (protocol.FoldingRange, bool) { r, ok := spanRange(pf, start, end) if ok { r.Kind = protocol.FoldingRangeKindComment @@ -210,7 +210,7 @@ func commentSpanRange(pf *cache.ParsedFile, start, end int) (protocol.FoldingRan // blockCommentSpan folds a multi-line block comment. The token records // only its start position, so the end line is derived from the text. -func blockCommentSpan(pf *cache.ParsedFile, idx int) (protocol.FoldingRange, bool) { +func blockCommentSpan(pf *store.ParsedFile, idx int) (protocol.FoldingRange, bool) { doc := pf.AST() tok := doc.Tokens[idx] startPos := toLSPPosition(pf, doc.TokenPosition(idx)) @@ -233,7 +233,7 @@ func blockCommentSpan(pf *cache.ParsedFile, idx int) (protocol.FoldingRange, boo // commentBlocks folds consecutive same-line comments on consecutive // source lines, and multi-line block comments, into comment fold ranges. -func commentBlocks(pf *cache.ParsedFile) []protocol.FoldingRange { +func commentBlocks(pf *store.ParsedFile) []protocol.FoldingRange { var ranges []protocol.FoldingRange doc := pf.AST() diff --git a/lsp/source/folding_test.go b/lsp/source/folding_test.go index 4322c44..dda69cb 100644 --- a/lsp/source/folding_test.go +++ b/lsp/source/folding_test.go @@ -11,7 +11,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // foldCase is one folding range expectation: the range of lines it covers. @@ -28,12 +29,12 @@ func foldingRanges(t *testing.T, src string) []protocol.FoldingRange { file := uri.File(filepath.Join(dir, "test.thrift")) require.NoError(t, os.WriteFile(filepath.Join(dir, "test.thrift"), []byte(src), 0o644)) - view := cache.NewView(uri.File(dir), cache.NewOverlayFS(cache.NewMemoizedFS()), nil) - view.Update(t.Context(), &cache.FileChange{ + view := store.NewView(uri.File(dir), vfs.NewOverlayFS(vfs.NewMemoizedFS()), nil) + view.Update(t.Context(), &vfs.FileChange{ URI: file, Version: 0, Content: []byte(src), - From: cache.FileChangeTypeInitialize, + From: vfs.FileChangeTypeInitialize, }) return Ranges(t.Context(), view, file) diff --git a/lsp/source/format.go b/lsp/source/format.go index 30b9bc4..a2adff4 100644 --- a/lsp/source/format.go +++ b/lsp/source/format.go @@ -8,8 +8,9 @@ import ( "go.lsp.dev/protocol" "github.com/karitham/thrift-ls/formatter" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/mapper" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // ErrNotParseable is returned by Format when the document has parse @@ -19,7 +20,7 @@ import ( var ErrNotParseable = errors.New("document does not parse") // Format returns the whole-document formatting of fh's content. -func Format(ctx context.Context, view *cache.View, fh cache.FileHandle, opts formatter.Options) (string, error) { +func Format(ctx context.Context, view *store.View, fh vfs.FileHandle, opts formatter.Options) (string, error) { pf, err := view.Parse(ctx, fh.URI()) if err != nil { return "", err @@ -35,7 +36,7 @@ func Format(ctx context.Context, view *cache.View, fh cache.FileHandle, opts for // 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, view *cache.View, fh cache.FileHandle, opts formatter.Options) (*protocol.TextEdit, error) { +func FormatDocument(ctx context.Context, view *store.View, fh vfs.FileHandle, opts formatter.Options) (*protocol.TextEdit, error) { content, err := fh.Content() if err != nil { return nil, err @@ -76,7 +77,7 @@ func FormatDocument(ctx context.Context, view *cache.View, 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, view *cache.View, fh cache.FileHandle, opts formatter.Options, rng protocol.Range) ([]protocol.TextEdit, error) { +func FormatRange(ctx context.Context, view *store.View, fh vfs.FileHandle, opts formatter.Options, rng protocol.Range) ([]protocol.TextEdit, error) { content, err := fh.Content() if err != nil { return nil, err @@ -278,7 +279,7 @@ 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, view *cache.View, fh cache.FileHandle, opts formatter.Options, pos protocol.Position) ([]protocol.TextEdit, error) { +func OnTypeFormat(ctx context.Context, view *store.View, fh vfs.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 @@ -294,7 +295,7 @@ func OnTypeFormat(ctx context.Context, view *cache.View, fh cache.FileHandle, op // enclosingConstruct returns the range of the top-level construct // containing pos — definitions never overlap, so at most one matches. -func enclosingConstruct(pf *cache.ParsedFile, pos protocol.Position) *protocol.Range { +func enclosingConstruct(pf *store.ParsedFile, pos protocol.Position) *protocol.Range { for _, n := range pf.AST().Nodes { rng := nodeRange(pf, n) diff --git a/lsp/source/highlight_test.go b/lsp/source/highlight_test.go index 1b87dc2..3622470 100644 --- a/lsp/source/highlight_test.go +++ b/lsp/source/highlight_test.go @@ -7,7 +7,8 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // TestHighlightSameFile pins document highlighting: the identifier at the @@ -15,13 +16,13 @@ import ( func TestHighlightSameFile(t *testing.T) { tests := []struct { name string - files []*cache.FileChange + files []*vfs.FileChange pos protocol.Position // cursor position in the first file wantLines []uint32 // highlighted lines in the first file }{ { name: "type name highlights definition and usages", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(`struct Gundam { 1: required string Name } @@ -29,36 +30,36 @@ func TestHighlightSameFile(t *testing.T) { struct StrikeRouge { 1: required Gundam pack 2: optional Gundam beamSaber -}`), From: cache.FileChangeTypeDidOpen}, +}`), From: vfs.FileChangeTypeDidOpen}, }, pos: protocol.Position{Line: 0, Character: 7}, wantLines: []uint32{0, 5, 6}, }, { name: "usage highlights the definition too", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(`struct Gundam { 1: required string Name } struct StrikeRouge { 1: required Gundam pack -}`), From: cache.FileChangeTypeDidOpen}, +}`), From: vfs.FileChangeTypeDidOpen}, }, pos: protocol.Position{Line: 5, Character: 14}, wantLines: []uint32{0, 5}, }, { name: "cross-file references are excluded", - files: []*cache.FileChange{ + files: []*vfs.FileChange{ {URI: "file:///tmp/gundam.thrift", Version: 0, Content: []byte(`struct Gundam { 1: required string Name -}`), From: cache.FileChangeTypeDidOpen}, +}`), From: vfs.FileChangeTypeDidOpen}, {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(`include "gundam.thrift" struct StrikeRouge { 1: required Gundam pack -}`), From: cache.FileChangeTypeDidOpen}, +}`), From: vfs.FileChangeTypeDidOpen}, }, pos: protocol.Position{Line: 0, Character: 7}, wantLines: []uint32{0}, @@ -67,7 +68,7 @@ struct StrikeRouge { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := cache.BuildViewForTest(tt.files) + view := store.BuildViewForTest(tt.files) highlights, err := Highlight(t.Context(), view, tt.files[0].URI, tt.pos) require.NoError(t, err) @@ -86,8 +87,8 @@ struct StrikeRouge { // TestHighlightUnresolvableType pins the minimal result for an identifier // without references: only the cursor word itself is highlighted. func TestHighlightUnresolvableType(t *testing.T) { - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte("struct Gundam {\n\t1: required UnknownType pack\n}"), From: vfs.FileChangeTypeDidOpen}, }) highlights, err := Highlight(t.Context(), view, "file:///tmp/main.thrift", protocol.Position{Line: 1, Character: 20}) diff --git a/lsp/source/hover.go b/lsp/source/hover.go index 43c41cc..3016ac6 100644 --- a/lsp/source/hover.go +++ b/lsp/source/hover.go @@ -7,14 +7,14 @@ import ( "go.lsp.dev/uri" "github.com/karitham/thrift-ls/formatter" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // Hover returns the formatted definition under the cursor: a type // reference, a constant value identifier, or a service reference. -func Hover(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) (res string, err error) { +func Hover(ctx context.Context, view *store.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 @@ -39,7 +39,7 @@ func formatNode(doc *syntax.Document, node syntax.Node) (string, error) { return formatter.FormatNode(doc, node, formatter.DefaultOptions()) } -func hoverService(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) (string, error) { +func hoverService(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) (string, error) { def, err := ix.ResolveService(ctx, pf, target.identifier()) if err != nil || def == nil { return "", err @@ -53,7 +53,7 @@ func hoverService(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, tar return formatNode(def.Parsed.AST(), svc) } -func hoverDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) (string, error) { +func hoverDefinition(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) (string, error) { ft := target.fieldType() if ft == nil { return "", nil @@ -71,7 +71,7 @@ func hoverDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, return formatNode(def.Parsed.AST(), def.Node) } -func hoverConstValue(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) (string, error) { +func hoverConstValue(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) (string, error) { def, err := ix.ResolveValue(ctx, pf, target.node.(*syntax.ConstValue)) if err != nil || def == nil { return "", err diff --git a/lsp/source/hover_test.go b/lsp/source/hover_test.go index 47bf2fa..ab2ae3f 100644 --- a/lsp/source/hover_test.go +++ b/lsp/source/hover_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestHover(t *testing.T) { @@ -31,9 +32,9 @@ service Svc extends Base { } const Color defaultColor = GREEN` - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), From: vfs.FileChangeTypeDidOpen}, }) posOf := func(file, marker string, offset ...int) protocol.Position { @@ -89,8 +90,8 @@ const Color defaultColor = GREEN` } func TestHoverUnresolvable(t *testing.T) { - view := cache.BuildViewForTest([]*cache.FileChange{ - {URI: "file:///tmp/test.thrift", Version: 0, Content: []byte("struct S {\n 1: Missing x\n}"), From: cache.FileChangeTypeDidOpen}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/test.thrift", Version: 0, Content: []byte("struct S {\n 1: Missing x\n}"), From: vfs.FileChangeTypeDidOpen}, }) got, err := Hover(t.Context(), view, "file:///tmp/test.thrift", protocol.Position{Line: 1, Character: 8}) diff --git a/lsp/source/links.go b/lsp/source/links.go index c04f07b..b336576 100644 --- a/lsp/source/links.go +++ b/lsp/source/links.go @@ -10,13 +10,13 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // Links returns the document links of a file, one per include and // cpp_include, targeting the resolved file. -func Links(ctx context.Context, view *cache.View, file uri.URI) []protocol.DocumentLink { +func Links(ctx context.Context, view *store.View, file uri.URI) []protocol.DocumentLink { pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil diff --git a/lsp/source/links_test.go b/lsp/source/links_test.go index 4f08575..fd2cacb 100644 --- a/lsp/source/links_test.go +++ b/lsp/source/links_test.go @@ -7,15 +7,16 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // buildSnapshot parses src as the file at URI and returns the snapshot. -func buildLinksSnapshot(t *testing.T, file uri.URI, src string) *cache.View { +func buildLinksSnapshot(t *testing.T, file uri.URI, src string) *store.View { t.Helper() - view := cache.BuildViewForTest([]*cache.FileChange{ - {URI: file, Version: 0, Content: []byte(src), From: cache.FileChangeTypeDidOpen}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: file, Version: 0, Content: []byte(src), From: vfs.FileChangeTypeDidOpen}, }) return view diff --git a/lsp/source/on_type_format_test.go b/lsp/source/on_type_format_test.go index efb7d7e..1684753 100644 --- a/lsp/source/on_type_format_test.go +++ b/lsp/source/on_type_format_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "github.com/karitham/thrift-ls/formatter" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // TestOnTypeFormat verifies that typing a closing brace formats the @@ -18,12 +19,12 @@ 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)} - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/f.thrift", Version: 0, Content: []byte(src), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) fh, err := view.ReadFile(t.Context(), "file:///tmp/f.thrift") @@ -43,12 +44,12 @@ func TestOnTypeFormatSkipsBrokenDocument(t *testing.T) { src := "struct S { 1: " pos := protocol.Position{Line: 0, Character: uint32(len(src))} - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/f.thrift", Version: 0, Content: []byte(src), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) fh, err := view.ReadFile(t.Context(), "file:///tmp/f.thrift") @@ -64,12 +65,12 @@ func TestOnTypeFormatSkipsBrokenDocument(t *testing.T) { func TestFormatSkipsBrokenDocument(t *testing.T) { src := "struct S { 1: " - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/f.thrift", Version: 0, Content: []byte(src), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) fh, err := view.ReadFile(t.Context(), "file:///tmp/f.thrift") diff --git a/lsp/source/provider.go b/lsp/source/provider.go index 9689f53..346ee60 100644 --- a/lsp/source/provider.go +++ b/lsp/source/provider.go @@ -8,7 +8,7 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -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, view *cache.View, file uri.URI, c Context) []Candidate + Candidates(ctx context.Context, view *store.View, file uri.URI, c Context) []Candidate } // providersFor returns the providers for a slot: the exact slot provider @@ -53,19 +53,19 @@ func providersFor(kind ContextKind) []Provider { type includeProvider struct{} -func (includeProvider) Candidates(_ context.Context, view *cache.View, file uri.URI, c Context) []Candidate { +func (includeProvider) Candidates(_ context.Context, view *store.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, view *cache.View, file uri.URI, c Context) []Candidate { +func (typeProvider) Candidates(ctx context.Context, view *store.View, file uri.URI, c Context) []Candidate { return typeCandidates(ctx, view, file, c) } type valueProvider struct{} -func (valueProvider) Candidates(ctx context.Context, view *cache.View, file uri.URI, c Context) []Candidate { +func (valueProvider) Candidates(ctx context.Context, view *store.View, file uri.URI, c Context) []Candidate { return valueCandidates(ctx, view, file, c.Doc) } @@ -73,7 +73,7 @@ type keywordProvider struct{} // Candidates returns the keyword snippets and every identifier token known // to the file (and its includes). -func (keywordProvider) Candidates(_ context.Context, view *cache.View, file uri.URI, _ Context) []Candidate { +func (keywordProvider) Candidates(_ context.Context, view *store.View, file uri.URI, _ Context) []Candidate { res := make([]Candidate, 0, len(keywords)+16) for text, format := range keywords { @@ -92,7 +92,7 @@ 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, view *cache.View, file uri.URI, _ Context) []Candidate { +func (fieldNameProvider) Candidates(_ context.Context, view *store.View, file uri.URI, _ Context) []Candidate { res := []Candidate{ {showText: "required", insertText: "required", format: protocol.InsertTextFormatPlainText}, {showText: "optional", insertText: "optional", 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, view *cache.View, file uri.URI, c Context) []Candidate { +func (annotationKeyProvider) Candidates(ctx context.Context, view *store.View, file uri.URI, c Context) []Candidate { keys := make(map[string]struct{}) collect := func(doc *syntax.Document) { @@ -148,7 +148,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, view *cache.View, file uri.URI, c Context) []Candidate { +func (serviceExtendsProvider) Candidates(ctx context.Context, view *store.View, file uri.URI, c Context) []Candidate { names := make(map[string]struct{}) collect := func(doc *syntax.Document) { diff --git a/lsp/source/reference.go b/lsp/source/reference.go index c887467..1e14133 100644 --- a/lsp/source/reference.go +++ b/lsp/source/reference.go @@ -9,23 +9,23 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // highlightKind maps a reference kind to the highlight type. -var highlightKind = map[cache.RefKind]protocol.DocumentHighlightKind{ - cache.RefFieldType: protocol.DocumentHighlightKindText, - cache.RefSignatureType: protocol.DocumentHighlightKindText, - cache.RefAnnotationType: protocol.DocumentHighlightKindText, - cache.RefConstValue: protocol.DocumentHighlightKindRead, - cache.RefServiceExtends: protocol.DocumentHighlightKindText, +var highlightKind = map[store.RefKind]protocol.DocumentHighlightKind{ + store.RefFieldType: protocol.DocumentHighlightKindText, + store.RefSignatureType: protocol.DocumentHighlightKindText, + store.RefAnnotationType: protocol.DocumentHighlightKindText, + store.RefConstValue: protocol.DocumentHighlightKindRead, + store.RefServiceExtends: protocol.DocumentHighlightKindText, } // Reference returns every usage of the symbol at pos, including usage // in files that include the definition. -func Reference(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) ([]protocol.Location, error) { +func Reference(ctx context.Context, view *store.View, file uri.URI, pos protocol.Position) ([]protocol.Location, error) { refs, err := searchReferences(ctx, view, file, pos) if err != nil { return nil, err @@ -44,7 +44,7 @@ func Reference(ctx context.Context, view *cache.View, file uri.URI, pos protocol } // Highlight returns the document highlight ranges for the symbol at pos. -func Highlight(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) ([]protocol.DocumentHighlight, error) { +func Highlight(ctx context.Context, view *store.View, file uri.URI, pos protocol.Position) ([]protocol.DocumentHighlight, error) { pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return nil, err @@ -95,7 +95,7 @@ func Highlight(ctx context.Context, view *cache.View, file uri.URI, pos protocol } // searchReferences dispatches to the reference search for the target kind. -func searchReferences(ctx context.Context, view *cache.View, file uri.URI, pos protocol.Position) ([]indexHit, error) { +func searchReferences(ctx context.Context, view *store.View, file uri.URI, pos protocol.Position) ([]indexHit, error) { pf, target, err := resolveTarget(ctx, view, file, pos) if err != nil { return nil, err @@ -118,7 +118,7 @@ func searchReferences(ctx context.Context, view *cache.View, file uri.URI, pos p } // searchTypeNameRefs resolves the type reference and finds all usages. -func searchTypeNameRefs(ctx context.Context, ix *sema.Index, view *cache.View, pf *cache.ParsedFile, target *target) ([]indexHit, error) { +func searchTypeNameRefs(ctx context.Context, ix *sema.Index, view *store.View, pf *store.ParsedFile, target *target) ([]indexHit, error) { ft := target.fieldType() if ft == nil { return nil, nil @@ -143,7 +143,7 @@ func searchTypeNameRefs(ctx context.Context, ix *sema.Index, view *cache.View, p return nil, err } - hits := []indexHit{{loc: loc, text: def.Name.Text, kind: cache.RefFieldType}} + hits := []indexHit{{loc: loc, text: def.Name.Text, kind: store.RefFieldType}} refs, err := ix.ReferencesTo(ctx, def, sema.RefKindsFor(def.Kind)...) if err != nil { @@ -159,7 +159,7 @@ func searchTypeNameRefs(ctx context.Context, ix *sema.Index, view *cache.View, p } // searchConstValueRefs resolves a const-value or enum-value reference. -func searchConstValueRefs(ctx context.Context, ix *sema.Index, view *cache.View, pf *cache.ParsedFile, target *target) ([]indexHit, error) { +func searchConstValueRefs(ctx context.Context, ix *sema.Index, view *store.View, pf *store.ParsedFile, target *target) ([]indexHit, error) { value := target.node.(*syntax.ConstValue) def, err := ix.ResolveValue(ctx, pf, value) @@ -176,9 +176,9 @@ func searchConstValueRefs(ctx context.Context, ix *sema.Index, view *cache.View, return nil, err } - hits := []indexHit{{loc: loc, text: def.Name.Text, kind: cache.RefConstValue}} + hits := []indexHit{{loc: loc, text: def.Name.Text, kind: store.RefConstValue}} - refs, err := ix.ReferencesTo(ctx, def, cache.RefConstValue) + refs, err := ix.ReferencesTo(ctx, def, store.RefConstValue) if err != nil { return nil, err } @@ -192,7 +192,7 @@ func searchConstValueRefs(ctx context.Context, ix *sema.Index, view *cache.View, } // searchServiceRefs finds the includes and extends referencing a service. -func searchServiceRefs(ctx context.Context, ix *sema.Index, view *cache.View, file uri.URI, svcName string) ([]indexHit, error) { +func searchServiceRefs(ctx context.Context, ix *sema.Index, view *store.View, file uri.URI, svcName string) ([]indexHit, error) { pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil, err @@ -203,7 +203,7 @@ func searchServiceRefs(ctx context.Context, ix *sema.Index, view *cache.View, fi return nil, nil } - refs, err := ix.ReferencesTo(ctx, def, cache.RefServiceExtends) + refs, err := ix.ReferencesTo(ctx, def, store.RefServiceExtends) if err != nil { return nil, err } @@ -213,7 +213,7 @@ func searchServiceRefs(ctx context.Context, ix *sema.Index, view *cache.View, fi // searchDefRefs handles references from a definition name: struct, union, // exception, enum, typedef, const, enum value, and service names. -func searchDefRefs(ctx context.Context, ix *sema.Index, view *cache.View, file uri.URI, pf *cache.ParsedFile, target *target) ([]indexHit, error) { +func searchDefRefs(ctx context.Context, ix *sema.Index, view *store.View, file uri.URI, pf *store.ParsedFile, target *target) ([]indexHit, error) { id := target.identifier() if id == nil { return nil, nil @@ -222,15 +222,15 @@ func searchDefRefs(ctx context.Context, ix *sema.Index, view *cache.View, file u parent := target.parent var def *sema.Resolved - var kinds []cache.RefKind + var kinds []store.RefKind switch parent.(type) { case *syntax.Const: def = sema.DefFromNode(pf, parent) - kinds = []cache.RefKind{cache.RefConstValue} + kinds = []store.RefKind{store.RefConstValue} case *syntax.EnumValue: def = sema.DefFromNode(pf, id) - kinds = []cache.RefKind{cache.RefConstValue} + kinds = []store.RefKind{store.RefConstValue} case *syntax.Service: svcName := id.Text if strings.Contains(svcName, ".") { @@ -261,7 +261,7 @@ func searchDefRefs(ctx context.Context, ix *sema.Index, view *cache.View, file u // Renaming an enum definition also touches value positions // qualified with the enum name ("Color.RED"). if kind == sema.DefinitionEnum { - kinds = append(kinds, cache.RefConstValue) + kinds = append(kinds, store.RefConstValue) } } @@ -300,7 +300,7 @@ func definitionKindOf(n syntax.Node) (sema.DefinitionKind, bool) { // hitLocs translates sema hits to LSP locations: each hit's parser- // coordinate span maps through its own file's mapper. -func hitLocs(ctx context.Context, view *cache.View, hits []sema.Hit) ([]indexHit, error) { +func hitLocs(ctx context.Context, view *store.View, hits []sema.Hit) ([]indexHit, error) { out := make([]indexHit, 0, len(hits)) for _, h := range hits { @@ -317,7 +317,7 @@ func hitLocs(ctx context.Context, view *cache.View, hits []sema.Hit) ([]indexHit // hitLoc translates one hit's parser-coordinate span to an LSP location // through the hit file's mapper. -func hitLoc(ctx context.Context, view *cache.View, h sema.Hit) (protocol.Location, error) { +func hitLoc(ctx context.Context, view *store.View, h sema.Hit) (protocol.Location, error) { pf, err := view.Parse(ctx, h.File) if err != nil { return protocol.Location{}, err @@ -331,7 +331,7 @@ func hitLoc(ctx context.Context, view *cache.View, h sema.Hit) (protocol.Locatio type indexHit struct { loc protocol.Location text string - kind cache.RefKind + kind store.RefKind } // validReferenceDefinitionType lists definition kinds that can have type diff --git a/lsp/source/reference_test.go b/lsp/source/reference_test.go index 08054cc..db45ad9 100644 --- a/lsp/source/reference_test.go +++ b/lsp/source/reference_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestReference(t *testing.T) { @@ -52,24 +53,24 @@ const user.UserType usermale = "male" const UserKind kind = "1" ` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View file uri.URI pos protocol.Position } diff --git a/lsp/source/rename.go b/lsp/source/rename.go index bf15ded..a9f0fb1 100644 --- a/lsp/source/rename.go +++ b/lsp/source/rename.go @@ -8,13 +8,13 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" ) // 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, view *cache.View, file uri.URI, pos protocol.Position) (res *protocol.Range, err error) { +func PrepareRename(ctx context.Context, view *store.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,7 +44,7 @@ func PrepareRename(ctx context.Context, view *cache.View, file uri.URI, pos prot // 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, view *cache.View, file uri.URI, pos protocol.Position, newName string) (res *protocol.WorkspaceEdit, err error) { +func Rename(ctx context.Context, view *store.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 diff --git a/lsp/source/rename_correctness_test.go b/lsp/source/rename_correctness_test.go index 939cb05..145b017 100644 --- a/lsp/source/rename_correctness_test.go +++ b/lsp/source/rename_correctness_test.go @@ -7,31 +7,32 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // TestRenameTransitiveInclude pins that a definition reached through a // 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) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/base.thrift", Version: 0, Content: []byte("struct User { 1: i32 id }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/mid.thrift", Version: 0, Content: []byte("include \"base.thrift\"\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/app.thrift", Version: 0, Content: []byte("include \"mid.thrift\"\nstruct S { 1: User u }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -53,18 +54,18 @@ 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) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/base.thrift", Version: 0, Content: []byte("struct User { 1: i32 id }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/app.thrift", Version: 0, Content: []byte("include \"base.thrift\"\nstruct User { 1: string name }\nstruct S { 1: base.User x, 2: User y }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -86,18 +87,18 @@ 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) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/colors.thrift", Version: 0, Content: []byte("enum Palette { RED }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/main.thrift", Version: 0, Content: []byte("include \"colors.thrift\"\nenum Local { RED }\nstruct S {\n 1: i32 a = Local.RED,\n 2: i32 b = colors.Palette.RED,\n}\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -119,18 +120,18 @@ 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) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/colors.thrift", Version: 0, Content: []byte("enum Color { RED }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/main.thrift", Version: 0, Content: []byte("include \"colors.thrift\"\nenum Color { BLUE }\nstruct S {\n 1: i32 a = Color.BLUE,\n 2: i32 b = colors.Color.RED,\n}\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/lsp/source/rename_enum_test.go b/lsp/source/rename_enum_test.go index 74113d0..86e2b47 100644 --- a/lsp/source/rename_enum_test.go +++ b/lsp/source/rename_enum_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // TestRenameEnumQualifiedValues pins that renaming an enum also updates @@ -66,17 +67,17 @@ func TestRenameEnumQualifiedValues(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - changes := make([]*cache.FileChange, 0, len(tt.files)) + changes := make([]*vfs.FileChange, 0, len(tt.files)) for file, src := range tt.files { - changes = append(changes, &cache.FileChange{ + changes = append(changes, &vfs.FileChange{ URI: uri.URI(file), Version: 0, Content: []byte(src), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }) } - view := cache.BuildViewForTest(changes) + view := store.BuildViewForTest(changes) edit, err := Rename(t.Context(), view, tt.cursor, tt.pos, tt.newName) require.NoError(t, err) diff --git a/lsp/source/rename_test.go b/lsp/source/rename_test.go index e0a6623..dccca1b 100644 --- a/lsp/source/rename_test.go +++ b/lsp/source/rename_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestPrepareRename(t *testing.T) { @@ -52,24 +53,24 @@ const user.UserType usermale = "male" const UserKind kind = "1" ` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View file uri.URI pos protocol.Position } @@ -305,24 +306,24 @@ const user.UserType usermale = "male" const UserKind kind = "1" ` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View file uri.URI pos protocol.Position newText string diff --git a/lsp/source/report.go b/lsp/source/report.go index 945a930..b0a3a68 100644 --- a/lsp/source/report.go +++ b/lsp/source/report.go @@ -6,15 +6,15 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" ) // ToProtocolDiagnostics translates one file's pipeline findings into LSP // wire diagnostics: spans map through the file's mapper to UTF-16 // columns, severities map onto the protocol scale, and the Source field // is set here — it is LSP presentation, not core data. -func ToProtocolDiagnostics(ctx context.Context, view *cache.View, file uri.URI, diags []sema.Diagnostic) ([]protocol.Diagnostic, error) { +func ToProtocolDiagnostics(ctx context.Context, view *store.View, file uri.URI, diags []sema.Diagnostic) ([]protocol.Diagnostic, error) { pf, err := view.Parse(ctx, file) if err != nil { return nil, err diff --git a/lsp/source/semantic.go b/lsp/source/semantic.go index 176e98e..8354d52 100644 --- a/lsp/source/semantic.go +++ b/lsp/source/semantic.go @@ -8,7 +8,7 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -47,7 +47,7 @@ func Legend() []string { // Tokens returns the delta-encoded semantic tokens of a file, in source // order. -func Tokens(ctx context.Context, view *cache.View, file uri.URI) ([]uint32, error) { +func Tokens(ctx context.Context, view *store.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_based_completion.go b/lsp/source/semantic_based_completion.go index a9bca02..7a12c1c 100644 --- a/lsp/source/semantic_based_completion.go +++ b/lsp/source/semantic_based_completion.go @@ -5,14 +5,14 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" ) 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, view *cache.View, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, bool, error) + Completion(ctx context.Context, view *store.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 0de3b52..61e58ff 100644 --- a/lsp/source/semantic_completion.go +++ b/lsp/source/semantic_completion.go @@ -7,15 +7,15 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // 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, view *cache.View, file uri.URI, c Context) []Candidate { +func typeCandidates(ctx context.Context, view *store.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 { @@ -137,7 +137,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, view *cache.View, file uri.URI, doc *syntax.Document) []Candidate { +func valueCandidates(ctx context.Context, view *store.View, file uri.URI, doc *syntax.Document) []Candidate { names := make(map[string]struct{}) collectValueNames := func(ast *syntax.Document) { for _, cst := range ast.Consts() { @@ -166,7 +166,7 @@ func valueCandidates(ctx context.Context, view *cache.View, file uri.URI, doc *s // includedFiles returns the files transitively included by file, per the // include graph. -func includedFiles(view *cache.View, file uri.URI) []uri.URI { +func includedFiles(view *store.View, file uri.URI) []uri.URI { var out []uri.URI visited := make(map[uri.URI]bool) diff --git a/lsp/source/semantic_test.go b/lsp/source/semantic_test.go index c27ea7b..aa671d3 100644 --- a/lsp/source/semantic_test.go +++ b/lsp/source/semantic_test.go @@ -8,7 +8,8 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // decodedToken is one semantic token in absolute coordinates. @@ -46,8 +47,8 @@ func decode(data []uint32) []decodedToken { func semanticTokens(t *testing.T, src string) []decodedToken { t.Helper() - view := cache.BuildViewForTest([]*cache.FileChange{ - {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(src), From: cache.FileChangeTypeDidOpen}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(src), From: vfs.FileChangeTypeDidOpen}, }) data, err := Tokens(t.Context(), view, "file:///tmp/main.thrift") @@ -147,8 +148,8 @@ 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) { - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/main.thrift", Version: 0, Content: []byte("const i32 A = 1\nconst i32 B = 2"), From: vfs.FileChangeTypeDidOpen}, }) data, err := Tokens(t.Context(), view, "file:///tmp/main.thrift") diff --git a/lsp/source/slot_completion_test.go b/lsp/source/slot_completion_test.go index 7ce7092..2b7b644 100644 --- a/lsp/source/slot_completion_test.go +++ b/lsp/source/slot_completion_test.go @@ -10,8 +10,9 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // lspPosOf returns the LSP position (0-based line, UTF-16 character) @@ -49,7 +50,7 @@ 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, view *cache.View, file string, pos protocol.Position) ([]string, protocol.Range, bool) { +func completionLabels(t *testing.T, view *store.View, file string, pos protocol.Position) ([]string, protocol.Range, bool) { t.Helper() fh, err := view.ReadFile(t.Context(), uri.URI(file)) @@ -70,7 +71,7 @@ func completionLabels(t *testing.T, view *cache.View, file string, pos protocol. } // completionItems runs the entry point and returns raw items. -func completionItems(t *testing.T, view *cache.View, file string, pos protocol.Position) ([]*CompletionItem, protocol.Range, bool) { +func completionItems(t *testing.T, view *store.View, file string, pos protocol.Position) ([]*CompletionItem, protocol.Range, bool) { t.Helper() fh, err := view.ReadFile(t.Context(), uri.URI(file)) @@ -88,7 +89,7 @@ func completionItems(t *testing.T, view *cache.View, file string, pos protocol.P // 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.View, string) { +func gundamSnapshot(t *testing.T, includePaths []string) (*store.View, string) { t.Helper() mainContent := `include "federation.gundam.thrift" @@ -113,8 +114,8 @@ exception BayFull { }` 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}, + &vfs.FileChange{URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(mainContent), From: vfs.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte(incContent), From: vfs.FileChangeTypeDidOpen}, ) return view, mainContent @@ -228,7 +229,7 @@ func labelsOf(cands []Candidate) []string { return labels } -func mustParse(t *testing.T, view *cache.View, file string) *syntax.Document { +func mustParse(t *testing.T, view *store.View, file string) *syntax.Document { t.Helper() pf, err := view.Parse(t.Context(), uri.URI(file)) @@ -248,8 +249,8 @@ func TestCompletionQualifiedValue(t *testing.T) { assert.NotEqual(t, mainContent, content) 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}, + &vfs.FileChange{URI: "file:///tmp/main.thrift", Version: 0, Content: []byte(content), From: vfs.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/federation.gundam.thrift", Version: 0, Content: []byte("struct MobileSuit {\n\t1: required string ModelName\n}"), From: vfs.FileChangeTypeDidOpen}, ) dotPos := lspPosOf(t, content, "const i32 LIMIT = ZeonForces.") @@ -275,7 +276,7 @@ func TestCompletionQualifiedValue(t *testing.T) { func TestCompletionKeywordFallback(t *testing.T) { view := buildSnapshot(t, nil, - &cache.FileChange{URI: "file:///tmp/empty.thrift", Version: 0, Content: []byte(""), From: cache.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/empty.thrift", Version: 0, Content: []byte(""), From: vfs.FileChangeTypeDidOpen}, ) labels, _, truncated := completionLabels(t, view, "file:///tmp/empty.thrift", protocol.Position{Line: 0, Character: 0}) @@ -311,7 +312,7 @@ func TestCompletionIncludePath(t *testing.T) { pos := lspPosOf(t, mainContent, "include \"fed") 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}, + &vfs.FileChange{URI: uri.File(filepath.Join(dir, "main.thrift")), Version: 0, Content: []byte(mainContent), From: vfs.FileChangeTypeDidOpen}, ) labels, rng, _ := completionLabels(t, view, uri.File(filepath.Join(dir, "main.thrift")).String(), pos) @@ -325,7 +326,7 @@ func TestCompletionIncludePath(t *testing.T) { mainContent2 := "include \"zeon/m" pos2 := lspPosOf(t, mainContent2, "include \"zeon/m") ss2 := buildSnapshot(t, []string{filepath.Join(dir, "zeon")}, - &cache.FileChange{URI: uri.File(filepath.Join(dir, "main.thrift")), Version: 0, Content: []byte(mainContent2), From: cache.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: uri.File(filepath.Join(dir, "main.thrift")), Version: 0, Content: []byte(mainContent2), From: vfs.FileChangeTypeDidOpen}, ) labels2, _, _ := completionLabels(t, ss2, uri.File(filepath.Join(dir, "main.thrift")).String(), pos2) @@ -336,7 +337,7 @@ func TestCompletionIncludePath(t *testing.T) { // wrapped edit range (the old whole-file prefix fallback bug). func TestCompletionNoPrefixUnderflow(t *testing.T) { view := buildSnapshot(t, nil, - &cache.FileChange{URI: "file:///tmp/underflow.thrift", Version: 0, Content: []byte("const X=1"), From: cache.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/underflow.thrift", Version: 0, Content: []byte("const X=1"), From: vfs.FileChangeTypeDidOpen}, ) _, rng, _ := completionLabels(t, view, "file:///tmp/underflow.thrift", protocol.Position{Line: 0, Character: 9}) @@ -350,7 +351,7 @@ func TestCompletionNonASCIIPrefix(t *testing.T) { pos := lspPosOf(t, content, "const X=1 😀") view := buildSnapshot(t, nil, - &cache.FileChange{URI: "file:///tmp/emoji.thrift", Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen}, + &vfs.FileChange{URI: "file:///tmp/emoji.thrift", Version: 0, Content: []byte(content), From: vfs.FileChangeTypeDidOpen}, ) _, rng, _ := completionLabels(t, view, "file:///tmp/emoji.thrift", pos) diff --git a/lsp/source/target.go b/lsp/source/target.go index 645e04e..115a289 100644 --- a/lsp/source/target.go +++ b/lsp/source/target.go @@ -8,7 +8,7 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -41,7 +41,7 @@ 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, view *cache.View, file uri.URI, pos protocol.Position) (*cache.ParsedFile, *target, error) { +func resolveTarget(ctx context.Context, view *store.View, file uri.URI, pos protocol.Position) (*store.ParsedFile, *target, error) { pf, err := view.Parse(ctx, file) if err != nil { return nil, nil, err @@ -126,7 +126,7 @@ func (t *target) identifier() *syntax.Identifier { } // jump builds an LSP location for a node. -func jump(file uri.URI, pf *cache.ParsedFile, node syntax.Node) protocol.Location { +func jump(file uri.URI, pf *store.ParsedFile, node syntax.Node) protocol.Location { return protocol.Location{ Range: nodeRange(pf, node), URI: file, @@ -137,7 +137,7 @@ 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, view *cache.View, file uri.URI, node syntax.Node) (protocol.Location, error) { +func jumpInFile(ctx context.Context, view *store.View, file uri.URI, node syntax.Node) (protocol.Location, error) { pf, err := view.Parse(ctx, file) if err != nil { return protocol.Location{}, err @@ -154,7 +154,7 @@ func jumpInFile(ctx context.Context, view *cache.View, file uri.URI, node syntax // file mapper, so character columns are UTF-16 code units as the protocol // requires. When the offset does not map (defensive), the rune column is // the fallback. -func toLSPPosition(pf *cache.ParsedFile, pos syntax.Position) protocol.Position { +func toLSPPosition(pf *store.ParsedFile, pos syntax.Position) protocol.Position { p, err := pf.Mapper().OffsetToLSPPosition(pos.Offset) if err != nil { return protocol.Position{Line: uint32(pos.Line - 1), Character: uint32(pos.Col - 1)} @@ -164,19 +164,19 @@ func toLSPPosition(pf *cache.ParsedFile, pos syntax.Position) protocol.Position } // toLSPRange converts a parser span to an LSP range with UTF-16 columns. -func toLSPRange(pf *cache.ParsedFile, start, end syntax.Position) protocol.Range { +func toLSPRange(pf *store.ParsedFile, start, end syntax.Position) protocol.Range { return protocol.Range{Start: toLSPPosition(pf, start), End: toLSPPosition(pf, end)} } // nodeRange converts a node span to an LSP range. -func nodeRange(pf *cache.ParsedFile, node syntax.Node) protocol.Range { +func nodeRange(pf *store.ParsedFile, node syntax.Node) protocol.Range { start, end := pf.AST().Range(node) return toLSPRange(pf, start, end) } // tokenRange converts a token's span to an LSP range. -func tokenRange(pf *cache.ParsedFile, tok *syntax.Token) protocol.Range { +func tokenRange(pf *store.ParsedFile, tok *syntax.Token) protocol.Range { if tok == nil { return protocol.Range{} } diff --git a/lsp/source/target_test.go b/lsp/source/target_test.go index 0882ab8..4aba753 100644 --- a/lsp/source/target_test.go +++ b/lsp/source/target_test.go @@ -5,7 +5,8 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestResolveTargetClassification(t *testing.T) { @@ -26,12 +27,12 @@ service Svc extends Base { User getUser(1: i64 id, 2: string name) throws (NotFound e) } ` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(src), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -92,12 +93,12 @@ service Svc extends Base { } func TestResolveTargetNoNode(t *testing.T) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/test.thrift", Version: 0, Content: []byte("struct S {\n 1: i32 a\n}\n\nconst i32 X = 1\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) // Position in the blank line between definitions: resolves to the diff --git a/lsp/source/token_completion.go b/lsp/source/token_completion.go index d659f83..9ba32eb 100644 --- a/lsp/source/token_completion.go +++ b/lsp/source/token_completion.go @@ -8,7 +8,7 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" ) var DefaultTokenCompletion Interface = &TokenCompletion{} @@ -58,7 +58,7 @@ 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, view *cache.View, cmp *CompletionRequest) ([]*CompletionItem, protocol.Range, bool, error) { +func (c *TokenCompletion) Completion(ctx context.Context, view *store.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 diff --git a/lsp/source/type_definition.go b/lsp/source/type_definition.go index 64c5038..b9fdae8 100644 --- a/lsp/source/type_definition.go +++ b/lsp/source/type_definition.go @@ -6,8 +6,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -15,7 +15,7 @@ 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, view *cache.View, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { +func TypeDefinition(ctx context.Context, view *store.View, file uri.URI, pos protocol.Position) (res []protocol.Location, err error) { res = make([]protocol.Location, 0) pf, target, err := resolveTarget(ctx, view, file, pos) @@ -39,7 +39,7 @@ func TypeDefinition(ctx context.Context, view *cache.View, file uri.URI, pos pro // declarationTypeDefinition jumps to the definition of the declared type of // a field, typedef, function, or const under the cursor. -func declarationTypeDefinition(ctx context.Context, ix *sema.Index, pf *cache.ParsedFile, target *target) ([]protocol.Location, error) { +func declarationTypeDefinition(ctx context.Context, ix *sema.Index, pf *store.ParsedFile, target *target) ([]protocol.Location, error) { var ft *syntax.FieldType switch parent := target.parent.(type) { diff --git a/lsp/source/type_definition_test.go b/lsp/source/type_definition_test.go index 4bb9bc7..40600a8 100644 --- a/lsp/source/type_definition_test.go +++ b/lsp/source/type_definition_test.go @@ -8,7 +8,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func TestTypeDefinition(t *testing.T) { @@ -51,24 +52,24 @@ typedef user.UserType UserKind const user.UserType usermale = "male" ` - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/api.thrift", Version: 0, Content: []byte(file2), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View file uri.URI pos protocol.Position } diff --git a/lsp/source/types.go b/lsp/source/types.go index cf3ec4a..b32be72 100644 --- a/lsp/source/types.go +++ b/lsp/source/types.go @@ -3,12 +3,12 @@ package source import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) type CompletionRequest struct { Pos protocol.Position - Fh cache.FileHandle + Fh vfs.FileHandle } type CompletionItem struct { diff --git a/lsp/source/utf16_test.go b/lsp/source/utf16_test.go index 1d0a174..d261544 100644 --- a/lsp/source/utf16_test.go +++ b/lsp/source/utf16_test.go @@ -20,16 +20,17 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // utf16Snapshot parses src as htt.thrift. -func utf16Snapshot(t *testing.T, src string) *cache.View { +func utf16Snapshot(t *testing.T, src string) *store.View { t.Helper() - return cache.BuildViewForTest([]*cache.FileChange{ - {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte(src), From: cache.FileChangeTypeDidOpen}, + return store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte(src), From: vfs.FileChangeTypeDidOpen}, }) } @@ -40,9 +41,9 @@ func utf16Snapshot(t *testing.T, src string) *cache.View { 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. - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte("/* 😀 */ struct HTT {}"), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/sakuragaoka.thrift", Version: 0, Content: []byte("include \"htt.thrift\"\nstruct LightMusicClub {\n 1: required HTT band\n}"), From: vfs.FileChangeTypeDidOpen}, }) locs, err := Definition(t.Context(), view, "file:///tmp/sakuragaoka.thrift", protocol.Position{ @@ -127,9 +128,9 @@ 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) { - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: "file:///tmp/htt.thrift", Version: 0, Content: []byte("/* 😀 */ struct HTT {}"), From: vfs.FileChangeTypeDidOpen}, + {URI: "file:///tmp/sakuragaoka.thrift", Version: 0, Content: []byte("include \"htt.thrift\"\nstruct LightMusicClub {\n 1: required HTT band\n}"), From: vfs.FileChangeTypeDidOpen}, }) edit, err := Rename(t.Context(), view, "file:///tmp/sakuragaoka.thrift", protocol.Position{ diff --git a/lsp/source/utils.go b/lsp/source/utils.go index 429fe77..e7d5e61 100644 --- a/lsp/source/utils.go +++ b/lsp/source/utils.go @@ -1,8 +1,8 @@ package source import ( - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/sema" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -30,7 +30,7 @@ func definitionMatches(n syntax.Node, kind sema.DefinitionKind) bool { // enumOfValue returns the enum declaring the value with the given name, or // nil. -func enumOfValue(pf *cache.ParsedFile, name string) *syntax.Enum { +func enumOfValue(pf *store.ParsedFile, name string) *syntax.Enum { id := pf.EnumValues()[name] if id == nil { return nil diff --git a/lsp/source/workspace.go b/lsp/source/workspace.go index 566180c..544a339 100644 --- a/lsp/source/workspace.go +++ b/lsp/source/workspace.go @@ -7,7 +7,7 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" ) // WorkspaceSymbols returns the workspace symbols matching query among the @@ -15,7 +15,7 @@ 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, view *cache.View, files []uri.URI, query string, maxResults int) []protocol.SymbolInformation { +func WorkspaceSymbols(ctx context.Context, view *store.View, files []uri.URI, query string, maxResults int) []protocol.SymbolInformation { res := make([]protocol.SymbolInformation, 0, 64) q := strings.ToLower(query) @@ -38,7 +38,7 @@ func WorkspaceSymbols(ctx context.Context, view *cache.View, files []uri.URI, qu // 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, view *cache.View, file uri.URI) []protocol.SymbolInformation { +func documentSymbolsFlat(ctx context.Context, view *store.View, file uri.URI) []protocol.SymbolInformation { syms := make([]protocol.SymbolInformation, 0, 16) for _, sym := range DocumentSymbols(ctx, view, file) { diff --git a/lsp/source/workspace_test.go b/lsp/source/workspace_test.go index 565203c..f2b9e68 100644 --- a/lsp/source/workspace_test.go +++ b/lsp/source/workspace_test.go @@ -12,7 +12,8 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // writeTree writes the file contents under dir and returns the directory. @@ -32,7 +33,7 @@ func writeTree(t *testing.T, files map[string]string) string { // openTree registers every file of dir with its view, like the server's // initialization walk does. When only is non-nil, only those file names // are registered. -func openTree(t *testing.T, session *cache.Session, dir string, only map[string]bool) { +func openTree(t *testing.T, session *store.Session, dir string, only map[string]bool) { t.Helper() folder := uri.File(dir) @@ -50,11 +51,11 @@ func openTree(t *testing.T, session *cache.Session, dir string, only map[string] content, err := os.ReadFile(path) require.NoError(t, err) - view.Update(t.Context(), &cache.FileChange{ + view.Update(t.Context(), &vfs.FileChange{ URI: uri.File(path), Version: 0, Content: content, - From: cache.FileChangeTypeInitialize, + From: vfs.FileChangeTypeInitialize, }) return nil @@ -63,7 +64,7 @@ func openTree(t *testing.T, session *cache.Session, dir string, only map[string] // allWorkspaceSymbols mirrors the server's Symbols handler: one snapshot // per view (folders ordered by URI), querying each view's known files. -func allWorkspaceSymbols(ctx context.Context, session *cache.Session, query string, maxResults int) []protocol.SymbolInformation { +func allWorkspaceSymbols(ctx context.Context, session *store.Session, query string, maxResults int) []protocol.SymbolInformation { var res []protocol.SymbolInformation views := session.Views() @@ -191,7 +192,7 @@ struct C { 1: string x }`, t.Run(tt.name, func(t *testing.T) { dir := writeTree(t, tt.files) - session := cache.NewSession(cache.NewMemoizedFS()) + session := store.NewSession(vfs.NewMemoizedFS()) if tt.nested { // Each top-level directory is a workspace folder. @@ -240,7 +241,7 @@ const i32 DEFAULT_HP = 100, typedef string PilotName`, }) - session := cache.NewSession(cache.NewMemoizedFS()) + session := store.NewSession(vfs.NewMemoizedFS()) openTree(t, session, dir, nil) file := uri.File(filepath.Join(dir, "shapes.thrift")) @@ -298,7 +299,7 @@ service Federation { }`, }) - session := cache.NewSession(cache.NewMemoizedFS()) + session := store.NewSession(vfs.NewMemoizedFS()) openTree(t, session, dir, nil) tests := []struct { @@ -352,7 +353,7 @@ exception BayFull { }`, }) - session := cache.NewSession(cache.NewMemoizedFS()) + session := store.NewSession(vfs.NewMemoizedFS()) openTree(t, session, dir, nil) syms := allWorkspaceSymbols(t.Context(), session, "", 0) diff --git a/lsp/symbols.go b/lsp/symbols.go index 0549a97..c6b2e85 100644 --- a/lsp/symbols.go +++ b/lsp/symbols.go @@ -5,12 +5,12 @@ import ( "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/lsp/source" + "github.com/karitham/thrift-ls/store" ) func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) (result protocol.DocumentSymbolSlice, err error) { - return withView(s.viewOf, params.TextDocument.URI, func(view *cache.View) (protocol.DocumentSymbolSlice, error) { + return withView(s.viewOf, params.TextDocument.URI, func(view *store.View) (protocol.DocumentSymbolSlice, error) { syms := source.DocumentSymbols(ctx, view, params.TextDocument.URI) result := make(protocol.DocumentSymbolSlice, 0, len(syms)) diff --git a/lsp/workspace.go b/lsp/workspace.go index 512faaa..cb22db7 100644 --- a/lsp/workspace.go +++ b/lsp/workspace.go @@ -11,8 +11,9 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/options" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // WorkspaceLoader discovers the projects in one LSP workspace folder. A @@ -65,7 +66,7 @@ type workspace struct { mu sync.Mutex folders map[uri.URI]*workspaceFolder model workspaceModel - views map[uri.URI]*cache.View + views map[uri.URI]*store.View documents map[uri.URI]struct{} } @@ -94,7 +95,7 @@ func newWorkspace(server *Server, loader WorkspaceLoader) *workspace { loader: loader, folders: make(map[uri.URI]*workspaceFolder), model: emptyWorkspaceModel(), - views: make(map[uri.URI]*cache.View), + views: make(map[uri.URI]*store.View), documents: make(map[uri.URI]struct{}), } } @@ -452,7 +453,7 @@ func (w *workspace) loadSync(ctx context.Context, folders []uri.URI) { // *.thrift file under the folder is a target, with no opinions of its own // so each root resolves through the ConfigSource. It keeps folder-based // sessions working with the workspace as the only workspace. -func defaultLoader(src cache.FileSource) WorkspaceLoader { +func defaultLoader(src vfs.FileSource) WorkspaceLoader { return func(ctx context.Context, folder uri.URI) (WorkspaceSnapshot, error) { var targets []uri.URI @@ -581,16 +582,16 @@ func (w *workspace) updateViewsLocked(ctx context.Context, changes map[uri.URI][ files := changes[root] slices.Sort(files) files = slices.Compact(files) - updates := make([]*cache.FileChange, len(files)) + updates := make([]*vfs.FileChange, len(files)) for i, file := range files { - updates[i] = &cache.FileChange{URI: file, From: cache.FileChangeTypeInitialize} + updates[i] = &vfs.FileChange{URI: file, From: vfs.FileChangeTypeInitialize} } w.server.postDiagnostics(ctx, view, view.Update(ctx, updates...)) } } -func (w *workspace) applyChanges(ctx context.Context, changes []*cache.FileChange, overlay bool) error { +func (w *workspace) applyChanges(ctx context.Context, changes []*vfs.FileChange, overlay bool) error { w.mu.Lock() defer w.mu.Unlock() @@ -605,7 +606,7 @@ func (w *workspace) applyChanges(ctx context.Context, changes []*cache.FileChang for _, change := range changes { previousRoot, previouslyOwned := w.model.ownerOf(change.URI, w.documents) if overlay { - if change.From == cache.FileChangeTypeDidClose { + if change.From == vfs.FileChangeTypeDidClose { delete(w.documents, change.URI) } else { w.documents[change.URI] = struct{}{} @@ -617,7 +618,7 @@ func (w *workspace) applyChanges(ctx context.Context, changes []*cache.FileChang continue } - if change.From == cache.FileChangeTypeDidClose && previouslyOwned { + if change.From == vfs.FileChangeTypeDidClose && previouslyOwned { if view := w.views[previousRoot]; view != nil { view.Evict(change.URI) evicted = append(evicted, change.URI) @@ -633,7 +634,7 @@ func (w *workspace) applyChanges(ctx context.Context, changes []*cache.FileChang return nil } -func (w *workspace) viewOf(file uri.URI) (*cache.View, error) { +func (w *workspace) viewOf(file uri.URI) (*store.View, error) { w.mu.Lock() defer w.mu.Unlock() @@ -666,7 +667,7 @@ func (w *workspace) rootContains(file uri.URI) bool { return ok } -func (w *workspace) files(view *cache.View) []uri.URI { +func (w *workspace) files(view *store.View) []uri.URI { w.mu.Lock() defer w.mu.Unlock() diff --git a/lsp/workspace_test.go b/lsp/workspace_test.go index 0b47acf..74fd13c 100644 --- a/lsp/workspace_test.go +++ b/lsp/workspace_test.go @@ -12,9 +12,9 @@ import ( "go.lsp.dev/protocol" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/options" "github.com/karitham/thrift-ls/resolver/resolvertest" + "github.com/karitham/thrift-ls/vfs" ) // Workspace lifecycle: initialize defers the load, folders add/remove. @@ -650,7 +650,7 @@ func TestCustomWatchedFilesDoNotReadUnownedEvents(t *testing.T) { target := uri.File("/workspace/project/api.thrift") unowned := uri.File("/workspace/project/stray.thrift") fs := &watchSpyFS{ - FileSource: cache.NewMemFS(resolvertest.Map{ + FileSource: vfs.NewMemFS(resolvertest.Map{ "/workspace/project/api.thrift": []byte("struct Before {}"), }.URIs()), forbidden: unowned, @@ -689,13 +689,13 @@ func TestCustomWatchedFilesDoNotReadUnownedEvents(t *testing.T) { } type watchSpyFS struct { - cache.FileSource + vfs.FileSource forbidden uri.URI reads atomic.Int32 contents atomic.Int32 } -func (fs *watchSpyFS) ReadFile(ctx context.Context, file uri.URI) (cache.FileHandle, error) { +func (fs *watchSpyFS) ReadFile(ctx context.Context, file uri.URI) (vfs.FileHandle, error) { fs.reads.Add(1) if file == fs.forbidden { return nil, errors.New("unowned file was read") @@ -710,7 +710,7 @@ func (fs *watchSpyFS) ReadFile(ctx context.Context, file uri.URI) (cache.FileHan } type watchSpyHandle struct { - cache.FileHandle + vfs.FileHandle contents *atomic.Int32 } diff --git a/main.go b/main.go index 1482704..16e082f 100644 --- a/main.go +++ b/main.go @@ -17,9 +17,10 @@ import ( "github.com/karitham/thrift-ls/doc" "github.com/karitham/thrift-ls/formatter" "github.com/karitham/thrift-ls/lsp" - "github.com/karitham/thrift-ls/lsp/cache" "github.com/karitham/thrift-ls/options" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" "go.lsp.dev/uri" ) @@ -198,7 +199,7 @@ func runLSP(ctx context.Context, cmd *cli.Command) error { lsp.InitLogger(logLevelValue) lspOpts := &lsp.Options{ - Files: cache.NewMemoizedFS(), + Files: vfs.NewMemoizedFS(), CLI: cliPatch, } // An explicit --config pins one document for every view, skipping @@ -366,14 +367,14 @@ func dumpIncludes(ctx context.Context, file string, cmd *cli.Command) error { patch = cliPatch.Apply(patch) - fs := cache.NewMemoizedFS() - sess := cache.NewSession(fs) + fs := vfs.NewMemoizedFS() + sess := store.NewSession(fs) sess.AddView(uri.File(filepath.Dir(abs)), derefStrings(patch.IncludePaths)) u := uri.File(abs) - err = sess.Update(ctx, []*cache.FileChange{ - {URI: u, Version: 0, Content: content, From: cache.FileChangeTypeDidOpen}, + err = sess.Update(ctx, []*vfs.FileChange{ + {URI: u, Version: 0, Content: content, From: vfs.FileChangeTypeDidOpen}, }) if err != nil { return err @@ -429,7 +430,7 @@ func dumpIncludes(ctx context.Context, file string, cmd *cli.Command) error { } // checkAction reports the diagnostics the language server computes for a -// thrift file or folder, through the same cache and checker pipeline the +// thrift file or folder, through the same store and checker pipeline the // LSP uses. Diagnostics print to stdout; the command exits 1 when any // error-severity diagnostic is found, so it can gate CI. Warnings do not // fail the check. diff --git a/resolver/resolvertest/resolvertest.go b/resolver/resolvertest/resolvertest.go index 79f7bbc..5795e5b 100644 --- a/resolver/resolvertest/resolvertest.go +++ b/resolver/resolvertest/resolvertest.go @@ -5,7 +5,7 @@ // resolvertest.Seed("/base/shared.thrift") // existence only // resolver.New(paths, resolver.WithFS(tree)) // fs.FS shape // resolver.New(paths, resolver.WithChecker(tree)) // Checker shape -// cache.NewMemFS(tree.URIs()) // FileSource shape +// vfs.NewMemFS(tree.URIs()) // FileSource shape // // The production path stays string paths end to end; URIs converts keys // for FileSource-based suites only. diff --git a/sema/action.go b/sema/action.go index 0c315b5..8fb6812 100644 --- a/sema/action.go +++ b/sema/action.go @@ -8,7 +8,7 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -57,7 +57,7 @@ func (p EnumValuesProvider) Actions(ctx context.Context, f File, span Span, repo // enumAt returns the enum declaration containing the selection start, or // nil when it lies outside every enum. -func enumAt(pf *cache.ParsedFile, span Span) *syntax.Enum { +func enumAt(pf *store.ParsedFile, span Span) *syntax.Enum { for _, enum := range pf.AST().Enums() { if pf.AST().Contains(enum, span.Start) { return enum @@ -70,7 +70,7 @@ func enumAt(pf *cache.ParsedFile, span Span) *syntax.Enum { // enumValueEdits appends " = N" to every member without an explicit value. // ok is false when the implicit values cannot be computed; the caller must // then not edit the enum, as the inserted values would be wrong. -func enumValueEdits(pf *cache.ParsedFile, enum *syntax.Enum) (edits []Edit, ok bool) { +func enumValueEdits(pf *store.ParsedFile, enum *syntax.Enum) (edits []Edit, ok bool) { for _, im := range EnumImplicitValues(enum) { if !im.Known { return nil, false @@ -166,7 +166,7 @@ func fieldQualifiers(field *syntax.Field, unionField bool) []syntax.TokenKind { // fieldQualifierAction builds the action switching the field to qualifier: // the edit replaces the span from the current qualifier keyword, or the // type start when the field is unqualified, up to the type start. -func fieldQualifierAction(pf *cache.ParsedFile, file uri.URI, field *syntax.Field, qualifier syntax.TokenKind) Action { +func fieldQualifierAction(pf *store.ParsedFile, file uri.URI, field *syntax.Field, qualifier syntax.TokenKind) Action { keyword := qualifier.String() start := pf.AST().TokenPosition(field.Type.TokStart()) diff --git a/sema/action_test.go b/sema/action_test.go index 6f4d3ba..f0aa27c 100644 --- a/sema/action_test.go +++ b/sema/action_test.go @@ -8,7 +8,8 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/protocol" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // applyEdits applies edits to content by byte offset, last-first. The @@ -31,7 +32,7 @@ func applyEdits(t *testing.T, content string, edits []Edit) string { // spanAt returns a one-point selection at a 1-based parser line/column, // with real offsets resolved through the file's mapper. -func spanAt(t *testing.T, pf *cache.ParsedFile, line, col int) Span { +func spanAt(t *testing.T, pf *store.ParsedFile, line, col int) Span { t.Helper() p, err := pf.Mapper().LSPPosToParserPosition(protocol.Position{Line: uint32(line - 1), Character: uint32(col - 1)}) @@ -109,12 +110,12 @@ enum E { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -142,12 +143,12 @@ enum E { func Test_EnumValuesProvider_QuickFixPromotion(t *testing.T) { content := "enum E { A, B = 1 }\n" - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -182,12 +183,12 @@ func Test_EnumValuesProvider_QuickFixPromotion(t *testing.T) { func Test_FieldQualifierProvider(t *testing.T) { content := "struct S {\n 1: i32 a,\n 2: required string b,\n 3: optional i64 c,\n}\n" - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/cycle_detect.go b/sema/cycle_detect.go index 0715480..8f5793d 100644 --- a/sema/cycle_detect.go +++ b/sema/cycle_detect.go @@ -8,7 +8,7 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -61,14 +61,14 @@ func cycleDiagnostic(inc Include) Diagnostic { type Include struct { file uri.URI include *syntax.Include - pf *cache.ParsedFile + pf *store.ParsedFile } // getIncludes collects the include closure of file into includesMap: the // include edges of every file reachable from file, parsed through the // view so the ParsedFiles (and the include edges parsing records) are // shared with the rest of the analysis. -func getIncludes(ctx context.Context, view *cache.View, file uri.URI, includesMap *map[uri.URI][]Include) error { +func getIncludes(ctx context.Context, view *store.View, file uri.URI, includesMap *map[uri.URI][]Include) error { pf, err := view.Parse(ctx, file) if err != nil { return err diff --git a/sema/cycle_detect_test.go b/sema/cycle_detect_test.go index 3a00874..71efb5d 100644 --- a/sema/cycle_detect_test.go +++ b/sema/cycle_detect_test.go @@ -10,17 +10,18 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) -func buildSnapshotForTest(t *testing.T, files []*cache.FileChange) *cache.View { +func buildSnapshotForTest(t *testing.T, files []*vfs.FileChange) *store.View { t.Helper() - c := cache.NewMemoizedFS() - fs := cache.NewOverlayFS(c) + c := vfs.NewMemoizedFS() + fs := vfs.NewOverlayFS(c) _ = fs.Update(t.Context(), files) - view := cache.NewView("file:///tmp", fs, nil) + view := store.NewView("file:///tmp", fs, nil) for _, f := range files { _, _ = view.Parse(t.Context(), f.URI) @@ -74,13 +75,13 @@ func runCycleCheck(t *testing.T, files map[string]string, root string) []cyclePa names := slices.Sorted(maps.Keys(files)) - changes := make([]*cache.FileChange, 0, len(files)) + changes := make([]*vfs.FileChange, 0, len(files)) for _, name := range names { - changes = append(changes, &cache.FileChange{ + changes = append(changes, &vfs.FileChange{ URI: uri.URI("file:///tmp/" + name), Version: 0, Content: []byte(files[name]), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }) } @@ -227,30 +228,30 @@ include "./test/address.thrift"` file2 := `include "../user.thrift"` file3 := `include "../user.thrift"` - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/test/goods.thrift", Version: 0, Content: []byte(file2), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///tmp/test/address.thrift", Version: 0, Content: []byte(file3), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) // Expected includes, parsed through the same snapshot so the ParsedFile // pointers match the ones getIncludes stores. - pfFor := func(uriStr string) *cache.ParsedFile { + pfFor := func(uriStr string) *store.ParsedFile { pf, err := view.Parse(t.Context(), uri.URI(uriStr)) require.NoError(t, err) diff --git a/sema/duplicate_check.go b/sema/duplicate_check.go index 81c83ff..21834c8 100644 --- a/sema/duplicate_check.go +++ b/sema/duplicate_check.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -138,7 +138,7 @@ func fieldNames(fields []*syntax.Field, kind string) []named { // checkNames reports a diagnostic on every name in a scope that repeats an // earlier one. -func checkNames(pf *cache.ParsedFile, defs []named) []Diagnostic { +func checkNames(pf *store.ParsedFile, defs []named) []Diagnostic { seen := make(map[string]bool) var ret []Diagnostic @@ -165,14 +165,14 @@ func checkNames(pf *cache.ParsedFile, defs []named) []Diagnostic { } // nameToken returns the token of an identifier. -func nameToken(pf *cache.ParsedFile, id *syntax.Identifier) *syntax.Token { +func nameToken(pf *store.ParsedFile, id *syntax.Identifier) *syntax.Token { return &pf.AST().Tokens[id.TokStart()] } // checkEnumValues reports enum members whose resolved value — an explicit // constant or the compiler's auto-increment — collides with an earlier // member's. -func checkEnumValues(pf *cache.ParsedFile, enum *syntax.Enum) []Diagnostic { +func checkEnumValues(pf *store.ParsedFile, enum *syntax.Enum) []Diagnostic { seen := make(map[int64]string) // value -> first member with it var ret []Diagnostic @@ -202,7 +202,7 @@ func checkEnumValues(pf *cache.ParsedFile, enum *syntax.Enum) []Diagnostic { // values in set literals, recursing into nested containers. The declared // type decides whether a list literal is a set: lists keep duplicates, // sets reject them. -func checkValueDuplicates(pf *cache.ParsedFile, value *syntax.ConstValue, typ *syntax.FieldType) []Diagnostic { +func checkValueDuplicates(pf *store.ParsedFile, value *syntax.ConstValue, typ *syntax.FieldType) []Diagnostic { var ret []Diagnostic switch value.Kind { @@ -280,7 +280,7 @@ func valueKey(v *syntax.ConstValue) string { // duplicateValueDiagnostic is the diagnostic for a repeated map key or set // value. -func duplicateValueDiagnostic(pf *cache.ParsedFile, v *syntax.ConstValue, kind string) Diagnostic { +func duplicateValueDiagnostic(pf *store.ParsedFile, v *syntax.ConstValue, kind string) Diagnostic { return Diagnostic{ Span: spanOfToken(&pf.AST().Tokens[v.TokStart()]), Severity: SeverityError, diff --git a/sema/duplicate_check_test.go b/sema/duplicate_check_test.go index 557bfa0..6400d67 100644 --- a/sema/duplicate_check_test.go +++ b/sema/duplicate_check_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) func Test_DuplicateCheck_Diagnostic(t *testing.T) { @@ -300,12 +300,12 @@ enum User {} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/enum_value_check.go b/sema/enum_value_check.go index 110e3a9..8418d83 100644 --- a/sema/enum_value_check.go +++ b/sema/enum_value_check.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -79,7 +79,7 @@ func (c *EnumValueCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnostic, } // enumValueNameToken returns the name token of an enum member. -func enumValueNameToken(pf *cache.ParsedFile, v *syntax.EnumValue) *syntax.Token { +func enumValueNameToken(pf *store.ParsedFile, v *syntax.EnumValue) *syntax.Token { return &pf.AST().Tokens[v.Name.TokStart()] } diff --git a/sema/enum_value_check_test.go b/sema/enum_value_check_test.go index 36d7f70..8a290fa 100644 --- a/sema/enum_value_check_test.go +++ b/sema/enum_value_check_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) func Test_EnumValueCheck_Diagnostic(t *testing.T) { @@ -80,12 +80,12 @@ func Test_EnumValueCheck_Diagnostic(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/fieldid_check_test.go b/sema/fieldid_check_test.go index 048a8c4..8fdfd82 100644 --- a/sema/fieldid_check_test.go +++ b/sema/fieldid_check_test.go @@ -8,7 +8,8 @@ import ( "github.com/stretchr/testify/assert" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func Test_FieldIDCheck_Diagnostic(t *testing.T) { @@ -39,18 +40,18 @@ service Demo { } ` - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View changeFiles []uri.URI } diff --git a/sema/fix_test.go b/sema/fix_test.go index b1446b3..8210326 100644 --- a/sema/fix_test.go +++ b/sema/fix_test.go @@ -8,22 +8,23 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) // openChange opens one file with content in a view. -func openChange(path, content string) []*cache.FileChange { - return []*cache.FileChange{{ +func openChange(path, content string) []*vfs.FileChange { + return []*vfs.FileChange{{ URI: uri.File(path), Version: 0, Content: []byte(content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }} } // addIncludeFixes returns the fixes the fixer offers for the undefined-type // diagnostics the analysis produced for file. -func addIncludeFixes(t *testing.T, view *cache.View, file uri.URI) []Fix { +func addIncludeFixes(t *testing.T, view *store.View, file uri.URI) []Fix { t.Helper() report := runOne(t, EachFile(&SemanticAnalysis{}), view, file) @@ -126,7 +127,7 @@ func TestFixAllEnumValues(t *testing.T) { b: []byte("enum Color {\n RED,\n GREEN = 2,\n BLUE,\n}\n"), } - view := cache.NewView(uri.File(folder), cache.NewMemFS(files), nil) + view := store.NewView(uri.File(folder), vfs.NewMemFS(files), nil) var written []byte diff --git a/sema/fixapply.go b/sema/fixapply.go index b0ac9e4..0b8815d 100644 --- a/sema/fixapply.go +++ b/sema/fixapply.go @@ -8,8 +8,9 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // maxFixPasses bounds FixAll's fixpoint loop. One pass may unlock another @@ -21,7 +22,7 @@ const maxFixPasses = 10 // fixes of the report's diagnostics for it, then the fixers' output for // each diagnostic. Fixes are edits in parser coordinates on the file's // current content. -func (p *Pipeline) FixesForFile(ctx context.Context, view *cache.View, file uri.URI, report Report) []Fix { +func (p *Pipeline) FixesForFile(ctx context.Context, view *store.View, file uri.URI, report Report) []Fix { pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil @@ -198,7 +199,7 @@ type FixResult struct { // content durable (write to disk, update the editor overlay) and visible // to view's file source before returning. FixAll drives view.Update // itself, so persist must not. -func (p *Pipeline) FixAll(ctx context.Context, view *cache.View, targets []uri.URI, persist func(context.Context, uri.URI, []byte) error) (FixResult, error) { +func (p *Pipeline) FixAll(ctx context.Context, view *store.View, targets []uri.URI, persist func(context.Context, uri.URI, []byte) error) (FixResult, error) { var res FixResult targets = slices.Clone(targets) @@ -230,7 +231,7 @@ func (p *Pipeline) FixAll(ctx context.Context, view *cache.View, targets []uri.U res.Remaining = report - changes := make([]*cache.FileChange, 0, len(targets)) + changes := make([]*vfs.FileChange, 0, len(targets)) passSkipped := make([]SkippedFix, 0, len(res.Skipped)) applied := 0 @@ -292,7 +293,7 @@ func (p *Pipeline) FixAll(ctx context.Context, view *cache.View, targets []uri.U applied += len(ok) res.Applied += len(ok) - changes = append(changes, &cache.FileChange{URI: u, Version: pass + 1, Content: out, From: cache.FileChangeTypeDidChange}) + changes = append(changes, &vfs.FileChange{URI: u, Version: pass + 1, Content: out, From: vfs.FileChangeTypeDidChange}) changed[u] = struct{}{} } diff --git a/sema/fixapply_test.go b/sema/fixapply_test.go index 0e77cc9..090f77d 100644 --- a/sema/fixapply_test.go +++ b/sema/fixapply_test.go @@ -8,8 +8,9 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) func spanOf(start, end int) Span { @@ -189,7 +190,7 @@ func TestFixAllGreenfield(t *testing.T) { uri.File(folder + "/c.thrift"): []byte("include \"a.thrift\"\n\nstruct Other {}\n"), } - view := cache.NewView(uri.File(folder), cache.NewMemFS(files), nil) + view := store.NewView(uri.File(folder), vfs.NewMemFS(files), nil) b := uri.File(folder + "/b.thrift") written := make(map[uri.URI][]byte) @@ -231,7 +232,7 @@ func TestFixAllParseErrorGuard(t *testing.T) { uri.File(folder + "/broken.thrift"): []byte("include \"a.thrift\"\n\nstruct Broken {\n 1: i32 a\n @@@\n}\n"), } - view := cache.NewView(uri.File(folder), cache.NewMemFS(files), nil) + view := store.NewView(uri.File(folder), vfs.NewMemFS(files), nil) broken := uri.File(folder + "/broken.thrift") @@ -267,7 +268,7 @@ func TestFixAllPersistFailure(t *testing.T) { uri.File(folder + "/c.thrift"): []byte("struct Foo {}\n"), } - view := cache.NewView(uri.File(folder), cache.NewMemFS(files), nil) + view := store.NewView(uri.File(folder), vfs.NewMemFS(files), nil) persist := func(_ context.Context, u uri.URI, content []byte) error { files[u] = content diff --git a/sema/include_shadow_check_test.go b/sema/include_shadow_check_test.go index a685c5f..8ba162f 100644 --- a/sema/include_shadow_check_test.go +++ b/sema/include_shadow_check_test.go @@ -9,7 +9,8 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func Test_IncludeShadowCheck(t *testing.T) { @@ -50,15 +51,15 @@ func Test_IncludeShadowCheck(t *testing.T) { require.NoError(t, os.MkdirAll(filepath.Join(folder, "camp"), 0o755)) filePath := writeThrift(t, folder, filepath.Join("camp", "main.thrift"), content) - view := cache.BuildViewForTestWithPaths([]string{ + view := store.BuildViewForTestWithPaths([]string{ filepath.Join(folder, "laios"), filepath.Join(folder, "senshi"), - }, []*cache.FileChange{ + }, []*vfs.FileChange{ { URI: uri.File(filePath), Version: 0, Content: []byte(content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/index.go b/sema/index.go index c9e84b5..ffc02b1 100644 --- a/sema/index.go +++ b/sema/index.go @@ -9,30 +9,30 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) // Index answers cross-file semantic queries over one view: definition // resolution and reference search. It composes per-file -// cache.FileIndexes over the include graph. +// store.FileIndexes over the include graph. // // An Index is cheap — construct one per request with NewIndex. Resolutions // 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 { - view *cache.View + view *store.View resolved map[resolveKey]*Resolved } // NewIndex returns an Index over the view's store. -func NewIndex(view *cache.View) *Index { +func NewIndex(view *store.View) *Index { return &Index{view: view} } // View returns the store view the index reads from. -func (x *Index) View() *cache.View { +func (x *Index) View() *store.View { return x.view } @@ -75,7 +75,7 @@ func (x *Index) memoize(file uri.URI, name string, kind resolveKind, def *Resolv // AST). It returns nil when the file cannot be read at all — an unresolved // include is a normal, reported condition, not a failure of the feature // answering a request. -func parseDefinitionFile(ctx context.Context, view *cache.View, file uri.URI) *cache.ParsedFile { +func parseDefinitionFile(ctx context.Context, view *store.View, file uri.URI) *store.ParsedFile { pf, err := view.Parse(ctx, file) if err != nil { slog.Debug("definition file unreadable", "file", file, "err", err) @@ -94,7 +94,7 @@ func parseDefinitionFile(ctx context.Context, view *cache.View, file uri.URI) *c // document, the definition identifier (jump target), and its kind. type Resolved struct { File uri.URI - Parsed *cache.ParsedFile + Parsed *store.ParsedFile // Name is the definition's identifier node, whose range is the jump // target. @@ -110,7 +110,7 @@ type Resolved struct { // ResolveType resolves a type reference to its definition, or returns nil // when unresolved (base types, unresolvable name). Files backing the // resolution may fail to read; those count as unresolved. -func (x *Index) ResolveType(ctx context.Context, from *cache.ParsedFile, ft *syntax.FieldType) (*Resolved, error) { +func (x *Index) ResolveType(ctx context.Context, from *store.ParsedFile, ft *syntax.FieldType) (*Resolved, error) { name := TypeReferenceName(ft) if name == "" || IsBasicType(name) { return nil, nil @@ -140,7 +140,7 @@ func (x *Index) ResolveType(ctx context.Context, from *cache.ParsedFile, ft *syn // An identifier that resolves to no definition returns nil — the type has // no classifiable kind, and existence is another check's job. A cyclic or // pathological chain stops at maxTypedefDepth steps. -func (x *Index) UnderlyingType(ctx context.Context, from *cache.ParsedFile, ft *syntax.FieldType) (*syntax.FieldType, *cache.ParsedFile) { +func (x *Index) UnderlyingType(ctx context.Context, from *store.ParsedFile, ft *syntax.FieldType) (*syntax.FieldType, *store.ParsedFile) { pf, cur := from, ft for range maxTypedefDepth { @@ -168,7 +168,7 @@ func (x *Index) UnderlyingType(ctx context.Context, from *cache.ParsedFile, ft * const maxTypedefDepth = 32 // 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) { +func (x *Index) resolveType(ctx context.Context, from *store.ParsedFile, name string) (*Resolved, error) { _, identifier := ParseIdent(from.URI(), from.AST().Includes(), name) for _, astFile := range definitionFiles(ctx, x.view, from.URI(), from.AST(), name) { @@ -192,7 +192,7 @@ func (x *Index) resolveType(ctx context.Context, from *cache.ParsedFile, name st // ResolveValue resolves a const-value identifier to its definition // (an enum value or a const), or returns nil when unresolved. -func (x *Index) ResolveValue(ctx context.Context, from *cache.ParsedFile, v *syntax.ConstValue) (*Resolved, error) { +func (x *Index) ResolveValue(ctx context.Context, from *store.ParsedFile, v *syntax.ConstValue) (*Resolved, error) { if v == nil || v.Kind != syntax.ValueIdent { return nil, nil } @@ -217,7 +217,7 @@ func (x *Index) ResolveValue(ctx context.Context, from *cache.ParsedFile, v *syn // resolveValue resolves a value-identifier text in from, without // memoization. -func (x *Index) resolveValue(ctx context.Context, from *cache.ParsedFile, text string) (*Resolved, error) { +func (x *Index) resolveValue(ctx context.Context, from *store.ParsedFile, text string) (*Resolved, error) { _, identifier := ParseIdent(from.URI(), from.AST().Includes(), text) identifier = BareName(identifier) @@ -241,7 +241,7 @@ func (x *Index) resolveValue(ctx context.Context, from *cache.ParsedFile, text s // ResolveService resolves a service name or extends reference, or returns // nil when unresolved. -func (x *Index) ResolveService(ctx context.Context, from *cache.ParsedFile, ident *syntax.Identifier) (*Resolved, error) { +func (x *Index) ResolveService(ctx context.Context, from *store.ParsedFile, ident *syntax.Identifier) (*Resolved, error) { if ident == nil { return nil, nil } @@ -262,7 +262,7 @@ func (x *Index) ResolveService(ctx context.Context, from *cache.ParsedFile, iden // resolveService resolves a service name text in from, without // memoization. -func (x *Index) resolveService(ctx context.Context, from *cache.ParsedFile, name string) (*Resolved, error) { +func (x *Index) resolveService(ctx context.Context, from *store.ParsedFile, name string) (*Resolved, error) { _, identifier := ParseIdent(from.URI(), from.AST().Includes(), name) for _, astFile := range definitionFiles(ctx, x.view, from.URI(), from.AST(), name) { dst := parseDefinitionFile(ctx, x.view, astFile) @@ -287,7 +287,7 @@ type Hit struct { // Kind is the grammar slot the reference sits in, so callers can tell // type hits from value hits (e.g. for highlight kinds). - Kind cache.RefKind + Kind store.RefKind } // References returns every occurrence of name in file and in every file @@ -295,7 +295,7 @@ type Hit struct { // The definition site is not included (no self-referencing hit). Hits are // matched by bare name only; use ReferencesTo for resolution-matched // results. -func (x *Index) References(ctx context.Context, file uri.URI, name string, kinds ...cache.RefKind) ([]Hit, error) { +func (x *Index) References(ctx context.Context, file uri.URI, name string, kinds ...store.RefKind) ([]Hit, error) { files := x.searchFiles(file) var out []Hit @@ -328,7 +328,7 @@ func (x *Index) References(ctx context.Context, file uri.URI, name string, kinds // qualifier resolves to this very enum; the hit covers only the enum // segment, so a rename rewrites the qualifier while keeping the member // name. -func (x *Index) ReferencesTo(ctx context.Context, def *Resolved, kinds ...cache.RefKind) ([]Hit, error) { +func (x *Index) ReferencesTo(ctx context.Context, def *Resolved, kinds ...store.RefKind) ([]Hit, error) { var out []Hit for _, f := range x.searchFiles(def.File) { @@ -338,7 +338,7 @@ func (x *Index) ReferencesTo(ctx context.Context, def *Resolved, kinds ...cache. } for _, r := range pf.Index().References() { - if r.Kind == cache.RefConstValue && def.Kind == DefinitionEnum && referenceKind(cache.RefConstValue, kinds) { + if r.Kind == store.RefConstValue && def.Kind == DefinitionEnum && referenceKind(store.RefConstValue, kinds) { // Value position qualified with the enum: only the enum // segment is rewritten. seg, off, ok := enumSegment(r.Name, def.Name.Text) @@ -467,16 +467,16 @@ func (x *Index) FindInWorkspace(ctx context.Context, name string) (*Resolved, er // resolves any declared type. Enum values and consts live in value // positions. Services are extends-only. Every other type can appear in // field, signature, and annotation-type slots. -func RefKindsFor(k DefinitionKind) []cache.RefKind { +func RefKindsFor(k DefinitionKind) []store.RefKind { switch k { case DefinitionException: - return []cache.RefKind{cache.RefSignatureType, cache.RefAnnotationType} + return []store.RefKind{store.RefSignatureType, store.RefAnnotationType} case DefinitionEnumValue, DefinitionConst: - return []cache.RefKind{cache.RefConstValue} + return []store.RefKind{store.RefConstValue} case DefinitionService: - return []cache.RefKind{cache.RefServiceExtends} + return []store.RefKind{store.RefServiceExtends} case DefinitionStruct, DefinitionUnion, DefinitionEnum, DefinitionTypedef: - return []cache.RefKind{cache.RefFieldType, cache.RefSignatureType, cache.RefAnnotationType} + return []store.RefKind{store.RefFieldType, store.RefSignatureType, store.RefAnnotationType} } return nil @@ -487,23 +487,23 @@ func RefKindsFor(k DefinitionKind) []cache.RefKind { // resolveReference resolves a reference to its definition, dispatching on // the grammar slot the reference sits in. Unresolvable references (parse // errors, unknown names) yield nil, not an error. -func (x *Index) resolveReference(ctx context.Context, pf *cache.ParsedFile, r cache.Reference) (*Resolved, error) { +func (x *Index) resolveReference(ctx context.Context, pf *store.ParsedFile, r store.Reference) (*Resolved, error) { switch r.Kind { - case cache.RefFieldType, cache.RefSignatureType, cache.RefAnnotationType: + case store.RefFieldType, store.RefSignatureType, store.RefAnnotationType: ident, ok := r.Node.(*syntax.Identifier) if !ok { return nil, nil } return x.ResolveType(ctx, pf, typeReference(ident.Text)) - case cache.RefConstValue: + case store.RefConstValue: v, ok := r.Node.(*syntax.ConstValue) if !ok { return nil, nil } return x.ResolveValue(ctx, pf, v) - case cache.RefServiceExtends: + case store.RefServiceExtends: ident, ok := r.Node.(*syntax.Identifier) if !ok { return nil, nil @@ -532,13 +532,13 @@ func sameDefinition(resolved, def *Resolved) bool { } // referenceKind reports whether k is in kinds; an empty kinds matches all. -func referenceKind(k cache.RefKind, kinds []cache.RefKind) bool { +func referenceKind(k store.RefKind, kinds []store.RefKind) bool { return len(kinds) == 0 || slices.Contains(kinds, k) } // enumSegmentHit builds a hit covering one segment (off, seg) of the // reference node's text, so a rename rewrites just that segment. -func enumSegmentHit(pf *cache.ParsedFile, node syntax.Node, off int, seg string) Hit { +func enumSegmentHit(pf *store.ParsedFile, node syntax.Node, off int, seg string) Hit { start, _ := pf.AST().Range(node) segStart := syntax.Position{ @@ -552,7 +552,7 @@ func enumSegmentHit(pf *cache.ParsedFile, node syntax.Node, off int, seg string) File: pf.URI(), Span: Span{Start: segStart, End: segEnd}, Text: seg, - Kind: cache.RefConstValue, + Kind: store.RefConstValue, } } @@ -575,7 +575,7 @@ func (x *Index) searchFiles(file uri.URI) []uri.URI { // matches returns hits for references in pf whose kind is in kinds and // whose bare name equals BareName(name). -func (x *Index) matches(pf *cache.ParsedFile, name string, kinds []cache.RefKind) []Hit { +func (x *Index) matches(pf *store.ParsedFile, name string, kinds []store.RefKind) []Hit { var out []Hit for _, r := range pf.Index().References() { @@ -619,7 +619,7 @@ func enumSegment(text, enumName string) (seg string, off int, ok bool) { } // defStruct maps a struct/union/exception definition. -func defStruct(pf *cache.ParsedFile, v *syntax.Struct) *Resolved { +func defStruct(pf *store.ParsedFile, v *syntax.Struct) *Resolved { return &Resolved{ File: pf.URI(), Parsed: pf, @@ -640,14 +640,14 @@ func structKind(k syntax.TokenKind) DefinitionKind { return DefinitionStruct } -func defFrom(pf *cache.ParsedFile, name *syntax.Identifier, node syntax.Node, kind DefinitionKind) *Resolved { +func defFrom(pf *store.ParsedFile, name *syntax.Identifier, node syntax.Node, kind DefinitionKind) *Resolved { return &Resolved{File: pf.URI(), Parsed: pf, Name: name, Node: node, Kind: kind} } // DefFromNode builds a Resolved from any top-level definition node. Use // when the concrete type and Kind are not known statically (e.g. // FindInWorkspace). -func DefFromNode(pf *cache.ParsedFile, n syntax.Node) *Resolved { +func DefFromNode(pf *store.ParsedFile, n syntax.Node) *Resolved { switch v := n.(type) { case *syntax.Struct: return defStruct(pf, v) diff --git a/sema/index_memo_test.go b/sema/index_memo_test.go index 8ee3572..758225b 100644 --- a/sema/index_memo_test.go +++ b/sema/index_memo_test.go @@ -7,8 +7,9 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // TestIndexResolutionMemo pins that Index resolutions are memoized per @@ -16,18 +17,18 @@ 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) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///base.thrift", Version: 0, Content: []byte("struct User { 1: i32 id }\nenum Color { RED = 1 }\nconst i32 MAX = 10\nservice Svc {}\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///app.thrift", Version: 0, Content: []byte("include \"base.thrift\"\nstruct User { 1: string name }\nstruct S { 1: base.User u, 2: i32 x = base.Color.RED }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -123,24 +124,24 @@ 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) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///a.thrift", Version: 0, Content: []byte("struct User { 1: i32 id }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///b.thrift", Version: 0, Content: []byte("struct User { 1: string name }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: "file:///main.thrift", Version: 0, Content: []byte("include \"a.thrift\"\ninclude \"b.thrift\"\nstruct S { 1: a.User x, 2: b.User y }\n"), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -150,7 +151,7 @@ func TestIndexMemoKeyIsolation(t *testing.T) { tests := []struct { name string - from *cache.ParsedFile + from *store.ParsedFile text string wantFile string }{ @@ -188,12 +189,12 @@ func TestSemanticAnalysisSkipsBrokenFile(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := cache.BuildViewForTest([]*cache.FileChange{ + view := store.BuildViewForTest([]*vfs.FileChange{ { URI: "file:///f.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/index_test.go b/sema/index_test.go index 7edbd49..a562568 100644 --- a/sema/index_test.go +++ b/sema/index_test.go @@ -7,8 +7,9 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) func TestIndex_ResolveType_SameFile(t *testing.T) { @@ -85,7 +86,7 @@ func TestIndex_References_Type(t *testing.T) { 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(view).References(ctx, fu("/t.thrift"), "User", cache.RefFieldType, cache.RefSignatureType) + hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "User", store.RefFieldType, store.RefSignatureType) require.NoError(t, err) require.Len(t, hits, 3) } @@ -95,7 +96,7 @@ func TestIndex_References_ExceptionRule(t *testing.T) { 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(view).References(ctx, fu("/t.thrift"), "Bad", cache.RefSignatureType) + hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "Bad", store.RefSignatureType) require.NoError(t, err) require.Len(t, hits, 1) assert.Equal(t, "Bad", hits[0].Text) @@ -106,7 +107,7 @@ func TestIndex_References_ConstValue(t *testing.T) { view := snap(t, "/t.thrift", "const i32 MAX = 10\nstruct Foo { 1: i32 id = MAX, }") _ = parseOne(t, view, fu("/t.thrift")) - hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "MAX", cache.RefConstValue) + hits, err := NewIndex(view).References(ctx, fu("/t.thrift"), "MAX", store.RefConstValue) require.NoError(t, err) require.Len(t, hits, 1) } @@ -120,7 +121,7 @@ func TestIndex_ReferencesToEnumValues(t *testing.T) { require.NoError(t, err) require.NotNil(t, def) - hits, err := NewIndex(view).ReferencesTo(ctx, def, cache.RefFieldType, cache.RefSignatureType, cache.RefConstValue) + hits, err := NewIndex(view).ReferencesTo(ctx, def, store.RefFieldType, store.RefSignatureType, store.RefConstValue) require.NoError(t, err) require.Len(t, hits, 2) for _, h := range hits { @@ -163,35 +164,35 @@ func TestIndex_FindInWorkspace(t *testing.T) { } func TestRefKindsFor(t *testing.T) { - assert.Equal(t, []cache.RefKind{cache.RefSignatureType, cache.RefAnnotationType}, RefKindsFor(DefinitionException)) - assert.Equal(t, []cache.RefKind{cache.RefFieldType, cache.RefSignatureType, cache.RefAnnotationType}, RefKindsFor(DefinitionStruct)) - assert.Equal(t, []cache.RefKind{cache.RefServiceExtends}, RefKindsFor(DefinitionService)) - assert.Equal(t, []cache.RefKind{cache.RefConstValue}, RefKindsFor(DefinitionConst)) + assert.Equal(t, []store.RefKind{store.RefSignatureType, store.RefAnnotationType}, RefKindsFor(DefinitionException)) + assert.Equal(t, []store.RefKind{store.RefFieldType, store.RefSignatureType, store.RefAnnotationType}, RefKindsFor(DefinitionStruct)) + assert.Equal(t, []store.RefKind{store.RefServiceExtends}, RefKindsFor(DefinitionService)) + assert.Equal(t, []store.RefKind{store.RefConstValue}, RefKindsFor(DefinitionConst)) } // --- helpers --- -func snap(t *testing.T, file, content string) *cache.View { +func snap(t *testing.T, file, content string) *store.View { t.Helper() - return cache.BuildViewForTest([]*cache.FileChange{{ - URI: fu(file), Version: 0, Content: []byte(content), From: cache.FileChangeTypeDidOpen, + return store.BuildViewForTest([]*vfs.FileChange{{ + URI: fu(file), Version: 0, Content: []byte(content), From: vfs.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.View { +func crossSnap(t *testing.T, fa, ca, fb, cb string) *store.View { t.Helper() - 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}, + view := store.BuildViewForTest([]*vfs.FileChange{ + {URI: fu(fb), Version: 0, Content: []byte(cb), From: vfs.FileChangeTypeDidOpen}, + {URI: fu(fa), Version: 0, Content: []byte(ca), From: vfs.FileChangeTypeDidOpen}, }) return view } func fu(p string) uri.URI { u, _ := uri.Parse("file://" + p); return u } -func parseOne(t *testing.T, view *cache.View, u uri.URI) *cache.ParsedFile { +func parseOne(t *testing.T, view *store.View, u uri.URI) *store.ParsedFile { t.Helper() pf, err := view.Parse(t.Context(), u) require.NoError(t, err) diff --git a/sema/non_scalar_map_key_check.go b/sema/non_scalar_map_key_check.go index 7fb348a..8663fcb 100644 --- a/sema/non_scalar_map_key_check.go +++ b/sema/non_scalar_map_key_check.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -40,7 +40,7 @@ func (c *NonScalarMapKeyCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagn return res, nil } -func (c *NonScalarMapKeyCheck) checkMapKeyScalar(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile, key *syntax.FieldType) *Diagnostic { +func (c *NonScalarMapKeyCheck) checkMapKeyScalar(ctx context.Context, view *store.View, ix *Index, pf *store.ParsedFile, key *syntax.FieldType) *Diagnostic { kind := c.mapKeyKind(ctx, view, ix, pf, key, 0) if kind == "" { return nil @@ -57,7 +57,7 @@ func (c *NonScalarMapKeyCheck) checkMapKeyScalar(ctx context.Context, view *cach // 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 (c *NonScalarMapKeyCheck) mapKeyKind(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile, key *syntax.FieldType, depth int) string { +func (c *NonScalarMapKeyCheck) mapKeyKind(ctx context.Context, view *store.View, ix *Index, pf *store.ParsedFile, key *syntax.FieldType, depth int) string { if key == nil { return "" } diff --git a/sema/pipeline.go b/sema/pipeline.go index e33c9d1..c50f226 100644 --- a/sema/pipeline.go +++ b/sema/pipeline.go @@ -8,19 +8,19 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" ) // File is one file's analysis inputs: its parsed tree plus the run's // shared state. type File struct { URI uri.URI - PF *cache.ParsedFile + PF *store.ParsedFile run *Run } // View returns the run's view (include resolver, dependency graph). -func (f File) View() *cache.View { +func (f File) View() *store.View { return f.run.view } @@ -90,7 +90,7 @@ func (fa fileAnalyzer) Analyze(ctx context.Context, run *Run) error { // Run is one analysis pass. Analyzers add findings to it. type Run struct { - view *cache.View + view *store.View files []uri.URI ix *Index report Report @@ -249,7 +249,7 @@ func Defaults() []Analyzer { // Run analyzes changed over view: one run, one shared Index across all // changed files and all analyzers. -func (p *Pipeline) Run(ctx context.Context, view *cache.View, changed []uri.URI) (Report, error) { +func (p *Pipeline) Run(ctx context.Context, view *store.View, changed []uri.URI) (Report, error) { run := &Run{view: view, files: changed, report: Report{}, cfg: p.cfg} var errs []error @@ -270,7 +270,7 @@ func (p *Pipeline) Run(ctx context.Context, view *cache.View, changed []uri.URI) // CodeActions returns the actions for span in file: quickfixes from the // report's diagnostics overlapping span (inline fixes first, then the // fixers), then the action providers' refactors. -func (p *Pipeline) CodeActions(ctx context.Context, view *cache.View, file uri.URI, span Span, report Report) []Action { +func (p *Pipeline) CodeActions(ctx context.Context, view *store.View, file uri.URI, span Span, report Report) []Action { pf, err := view.Parse(ctx, file) if err != nil || pf.AST() == nil { return nil diff --git a/sema/pipeline_test.go b/sema/pipeline_test.go index 057ac49..98daa54 100644 --- a/sema/pipeline_test.go +++ b/sema/pipeline_test.go @@ -8,14 +8,15 @@ import ( "github.com/stretchr/testify/require" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) var _ func(Config) *Pipeline = DefaultPipeline // runOne runs a single analyzer over view and files: the shape check // tests use to pin one check's behavior in isolation. -func runOne(t *testing.T, a Analyzer, view *cache.View, files ...uri.URI) Report { +func runOne(t *testing.T, a Analyzer, view *store.View, files ...uri.URI) Report { t.Helper() report, err := New(Config{}, []Analyzer{a}).Run(t.Context(), view, files) @@ -62,14 +63,14 @@ func cmpAll(ds []Diagnostic) []diagCmp { // 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.View { +func buildFolderSnapshotForTest(t *testing.T, folder string, files []*vfs.FileChange) *store.View { t.Helper() - c := cache.NewMemoizedFS() - fs := cache.NewOverlayFS(c) + c := vfs.NewMemoizedFS() + fs := vfs.NewOverlayFS(c) _ = fs.Update(t.Context(), files) - view := cache.NewView(uri.File(folder), fs, nil) + view := store.NewView(uri.File(folder), fs, nil) for _, f := range files { _, _ = view.Parse(t.Context(), f.URI) @@ -89,7 +90,7 @@ func writeThrift(t *testing.T, folder, name, content string) string { } // analyzeOne runs the full semantic analysis over one file. -func analyzeOne(t *testing.T, view *cache.View, file uri.URI) []Diagnostic { +func analyzeOne(t *testing.T, view *store.View, file uri.URI) []Diagnostic { t.Helper() return runOne(t, EachFile(&SemanticAnalysis{}), view, file)[file] diff --git a/sema/sema.go b/sema/sema.go index 1a01b12..4be7f29 100644 --- a/sema/sema.go +++ b/sema/sema.go @@ -6,7 +6,7 @@ package sema import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -18,7 +18,7 @@ type Span struct { } // SpanOf returns the source span of a node in the parsed file. -func SpanOf(pf *cache.ParsedFile, node syntax.Node) Span { +func SpanOf(pf *store.ParsedFile, node syntax.Node) Span { start, end := pf.AST().Range(node) return Span{Start: start, End: end} diff --git a/sema/semantic_analysis.go b/sema/semantic_analysis.go index 165a441..c156b08 100644 --- a/sema/semantic_analysis.go +++ b/sema/semantic_analysis.go @@ -6,7 +6,7 @@ import ( "slices" "strings" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -28,7 +28,7 @@ func (s *SemanticAnalysis) AnalyzeFile(ctx context.Context, f File) ([]Diagnosti // resolving to no enum value or const at any nesting depth, and field defaults // or const values whose kind — at any literal depth — mismatches the declared // type's underlying kind. -func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, view *cache.View, ix *Index, pf *cache.ParsedFile) []Diagnostic { +func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, view *store.View, ix *Index, pf *store.ParsedFile) []Diagnostic { res := make([]Diagnostic, 0) syntax.Walk(pf.AST(), func(n syntax.Node) bool { @@ -57,8 +57,8 @@ func (s *SemanticAnalysis) checkDefinitionExist(ctx context.Context, view *cache // resolves to no definition. Matching the upfluence compiler, the name of // a structured annotation must be a declared type: the compiler errors at // parse time ("Type %s does not exist"); here the semantic pass owns it. -func (s *SemanticAnalysis) checkAnnotationTypeExist(ctx context.Context, view *cache.View, ix *Index, - pf *cache.ParsedFile, sa *syntax.StructuredAnnotation, +func (s *SemanticAnalysis) checkAnnotationTypeExist(ctx context.Context, view *store.View, ix *Index, + pf *store.ParsedFile, sa *syntax.StructuredAnnotation, ) (res []Diagnostic) { if sa == nil || sa.Name == nil { return res @@ -79,8 +79,8 @@ func (s *SemanticAnalysis) checkAnnotationTypeExist(ctx context.Context, view *c }) } -func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, view *cache.View, ix *Index, - pf *cache.ParsedFile, cst *syntax.ConstValue, +func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, view *store.View, ix *Index, + pf *store.ParsedFile, cst *syntax.ConstValue, ) (res []Diagnostic) { if cst == nil || cst.Kind != syntax.ValueIdent { return res @@ -112,7 +112,7 @@ func (s *SemanticAnalysis) checkConstValueExist(ctx context.Context, view *cache // whose scope expect resolves in — the two part ways once the type walks // into an include. Existence of identifier values is checkConstValueExist's // job; this only classifies kinds. -func (s *SemanticAnalysis) checkValueType(ctx context.Context, ix *Index, reportPf, scopePf *cache.ParsedFile, expect *syntax.FieldType, value *syntax.ConstValue) []Diagnostic { +func (s *SemanticAnalysis) checkValueType(ctx context.Context, ix *Index, reportPf, scopePf *store.ParsedFile, expect *syntax.FieldType, value *syntax.ConstValue) []Diagnostic { if value == nil { return nil } @@ -165,7 +165,7 @@ func (s *SemanticAnalysis) checkValueType(ctx context.Context, ix *Index, report // checkMapEntries validates each entry of a map literal against the map // type's resolved key and value types. -func (s *SemanticAnalysis) checkMapEntries(ctx context.Context, ix *Index, reportPf, scopePf *cache.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) []Diagnostic { +func (s *SemanticAnalysis) checkMapEntries(ctx context.Context, ix *Index, reportPf, scopePf *store.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) []Diagnostic { var res []Diagnostic for _, entry := range value.Map { @@ -178,7 +178,7 @@ func (s *SemanticAnalysis) checkMapEntries(ctx context.Context, ix *Index, repor // checkListEntries validates each element of a list literal against the // container's resolved element type. -func (s *SemanticAnalysis) checkListEntries(ctx context.Context, ix *Index, reportPf, scopePf *cache.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) []Diagnostic { +func (s *SemanticAnalysis) checkListEntries(ctx context.Context, ix *Index, reportPf, scopePf *store.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) []Diagnostic { var res []Diagnostic for _, elem := range value.List { @@ -191,7 +191,7 @@ func (s *SemanticAnalysis) checkListEntries(ctx context.Context, ix *Index, repo // checkStructEntries validates a struct-valued map literal: keys must name // fields of the resolved definition, and each value is classified against // the field's own type. -func (s *SemanticAnalysis) checkStructEntries(ctx context.Context, ix *Index, reportPf *cache.ParsedFile, typeName string, fields []*syntax.Field, structPf *cache.ParsedFile, value *syntax.ConstValue) []Diagnostic { +func (s *SemanticAnalysis) checkStructEntries(ctx context.Context, ix *Index, reportPf *store.ParsedFile, typeName string, fields []*syntax.Field, structPf *store.ParsedFile, value *syntax.ConstValue) []Diagnostic { byName := make(map[string]*syntax.Field, len(fields)) for _, f := range fields { if f.Name != nil { @@ -222,7 +222,7 @@ func (s *SemanticAnalysis) checkStructEntries(ctx context.Context, ix *Index, re } // kindMismatch reports a literal whose kind contradicts the target type. -func kindMismatch(pf *cache.ParsedFile, value *syntax.ConstValue, ut *syntax.FieldType) []Diagnostic { +func kindMismatch(pf *store.ParsedFile, value *syntax.ConstValue, ut *syntax.FieldType) []Diagnostic { return []Diagnostic{*mismatchDiagnostic(pf, value, underlyingName(ut), gotName(value))} } @@ -239,7 +239,7 @@ func gotName(value *syntax.ConstValue) string { // unknownFieldDiagnostic reports a struct-valued literal keyed by a name // that is not a field of the definition. name is the key text with its // quotes stripped. -func unknownFieldDiagnostic(pf *cache.ParsedFile, key *syntax.ConstValue, typeName, name string) Diagnostic { +func unknownFieldDiagnostic(pf *store.ParsedFile, key *syntax.ConstValue, typeName, name string) Diagnostic { return Diagnostic{ Span: SpanOf(pf, key), Severity: SeverityError, @@ -267,7 +267,7 @@ func isBase(ft *syntax.FieldType, base syntax.TokenKind) bool { // types resolve in — the compiler accepts map literals, keyed by field // name, for struct-valued constants and field defaults. nil fields when // the type is not a struct-like definition. -func structFields(ctx context.Context, ix *Index, ut *syntax.FieldType, typePf *cache.ParsedFile) ([]*syntax.Field, *cache.ParsedFile) { +func structFields(ctx context.Context, ix *Index, ut *syntax.FieldType, typePf *store.ParsedFile) ([]*syntax.Field, *store.ParsedFile) { if ut == nil || ut.Kind != syntax.TypeIdent { return nil, nil } @@ -293,7 +293,7 @@ func structFields(ctx context.Context, ix *Index, ut *syntax.FieldType, typePf * // the compiler substitutes bool-const identifiers before validating — so // the reference must resolve, in the referencing file's scope, to a const // of bool type. -func identValueMatches(ctx context.Context, ix *Index, pf *cache.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) bool { +func identValueMatches(ctx context.Context, ix *Index, pf *store.ParsedFile, ut *syntax.FieldType, value *syntax.ConstValue) bool { if !isBase(ut, syntax.TokenBool) { return true } @@ -317,7 +317,7 @@ func identValueMatches(ctx context.Context, ix *Index, pf *cache.ParsedFile, ut // intValueMatches reports whether an integer literal fits the underlying // type: an integer base, or an enum — the compiler accepts int constants // in enum positions. -func intValueMatches(ctx context.Context, ix *Index, ut *syntax.FieldType, typePf *cache.ParsedFile, value *syntax.ConstValue) bool { +func intValueMatches(ctx context.Context, ix *Index, ut *syntax.FieldType, typePf *store.ParsedFile, value *syntax.ConstValue) bool { if value.Text == "true" || value.Text == "false" { return isBase(ut, syntax.TokenBool) } @@ -384,7 +384,7 @@ func kindName(kind syntax.ConstValueKind) string { return "unknown" } -func mismatchDiagnostic(pf *cache.ParsedFile, value *syntax.ConstValue, expect, got string) *Diagnostic { +func mismatchDiagnostic(pf *store.ParsedFile, value *syntax.ConstValue, expect, got string) *Diagnostic { return &Diagnostic{ Span: SpanOf(pf, value), Severity: SeverityError, @@ -396,8 +396,8 @@ func mismatchDiagnostic(pf *cache.ParsedFile, value *syntax.ConstValue, expect, // checkTypeExist reports a single type reference that resolves to no // definition. The walk visits every FieldType individually — including // container element types — so this needs no recursion. -func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, view *cache.View, ix *Index, - pf *cache.ParsedFile, ft *syntax.FieldType, +func (s *SemanticAnalysis) checkTypeExist(ctx context.Context, view *store.View, ix *Index, + pf *store.ParsedFile, ft *syntax.FieldType, ) (res []Diagnostic) { if ft == nil || ft.Kind != syntax.TypeIdent { return res diff --git a/sema/semantic_analysis_test.go b/sema/semantic_analysis_test.go index 7689894..6550df4 100644 --- a/sema/semantic_analysis_test.go +++ b/sema/semantic_analysis_test.go @@ -8,7 +8,8 @@ import ( "github.com/stretchr/testify/assert" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" + "github.com/karitham/thrift-ls/vfs" ) func Test_SemanticAnalysis_Diagnostic(t *testing.T) { @@ -52,18 +53,18 @@ struct TestUUID { 1: required uuid id } ` - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(file1), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) type args struct { ctx context.Context - view *cache.View + view *store.View changeFiles []uri.URI } @@ -226,12 +227,12 @@ func Test_NonScalarMapKeyCheck(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -287,12 +288,12 @@ func Test_SemanticAnalysis_StructuredAnnotations(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -353,12 +354,12 @@ func Test_SemanticAnalysis_WalkCoverage(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/user.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) @@ -492,12 +493,12 @@ func Test_SemanticAnalysis_ConstValueType(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - view := buildSnapshotForTest(t, []*cache.FileChange{ + view := buildSnapshotForTest(t, []*vfs.FileChange{ { URI: "file:///tmp/orth.thrift", Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/unused_include_check.go b/sema/unused_include_check.go index d9d6326..5f892b3 100644 --- a/sema/unused_include_check.go +++ b/sema/unused_include_check.go @@ -6,7 +6,7 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -28,7 +28,7 @@ func (c *UnusedIncludeCheck) AnalyzeFile(ctx context.Context, f File) ([]Diagnos // 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, f File, pf *cache.ParsedFile) []Diagnostic { +func unusedIncludeDiagnostics(ctx context.Context, f File, pf *store.ParsedFile) []Diagnostic { includes := pf.AST().Includes() if len(includes) == 0 { return nil @@ -94,7 +94,7 @@ func lineSpan(content []byte, pos syntax.Position) Span { // document resolves into. Resolution goes through the run's shared // cross-file index, which handles both qualified ("base.Type") and // unqualified names that resolve through the include chain. -func usedIncludes(ctx context.Context, f File, pf *cache.ParsedFile) map[*syntax.Include]bool { +func usedIncludes(ctx context.Context, f File, pf *store.ParsedFile) map[*syntax.Include]bool { resolver := f.View().Resolver() includeByFile := make(map[uri.URI]*syntax.Include) @@ -127,12 +127,12 @@ func usedIncludes(ctx context.Context, f File, pf *cache.ParsedFile) map[*syntax // resolveReferenceFile returns the file the reference resolves to, or // false when it resolves nowhere or into the current file. Type, const // value, and service references resolve through their own finder. -func resolveReferenceFile(ctx context.Context, ix *Index, pf *cache.ParsedFile, ref cache.Reference) (uri.URI, bool) { +func resolveReferenceFile(ctx context.Context, ix *Index, pf *store.ParsedFile, ref store.Reference) (uri.URI, bool) { var def *Resolved var err error switch ref.Kind { - case cache.RefFieldType, cache.RefSignatureType, cache.RefAnnotationType: + case store.RefFieldType, store.RefSignatureType, store.RefAnnotationType: id, ok := ref.Node.(*syntax.Identifier) if !ok { return "", false @@ -140,14 +140,14 @@ func resolveReferenceFile(ctx context.Context, ix *Index, pf *cache.ParsedFile, ft := &syntax.FieldType{Kind: syntax.TypeIdent, Ident: id} def, err = ix.ResolveType(ctx, pf, ft) - case cache.RefConstValue: + case store.RefConstValue: cv, ok := ref.Node.(*syntax.ConstValue) if !ok { return "", false } def, err = ix.ResolveValue(ctx, pf, cv) - case cache.RefServiceExtends: + case store.RefServiceExtends: id, ok := ref.Node.(*syntax.Identifier) if !ok { return "", false diff --git a/sema/unused_include_check_test.go b/sema/unused_include_check_test.go index e0b93f2..567a2d7 100644 --- a/sema/unused_include_check_test.go +++ b/sema/unused_include_check_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/vfs" ) func Test_UnusedIncludeCheck(t *testing.T) { @@ -81,12 +81,12 @@ func Test_UnusedIncludeCheck(t *testing.T) { filePath := writeThrift(t, folder, "user.thrift", tt.content) - view := buildFolderSnapshotForTest(t, folder, []*cache.FileChange{ + view := buildFolderSnapshotForTest(t, folder, []*vfs.FileChange{ { URI: uri.File(filePath), Version: 0, Content: []byte(tt.content), - From: cache.FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }) diff --git a/sema/utils.go b/sema/utils.go index b614fd5..ac487ef 100644 --- a/sema/utils.go +++ b/sema/utils.go @@ -6,7 +6,7 @@ import ( "go.lsp.dev/uri" - "github.com/karitham/thrift-ls/lsp/cache" + "github.com/karitham/thrift-ls/store" "github.com/karitham/thrift-ls/syntax" ) @@ -30,7 +30,7 @@ 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, view *cache.View, file uri.URI, ast *syntax.Document, name string) []uri.URI { +func definitionFiles(ctx context.Context, view *store.View, file uri.URI, ast *syntax.Document, name string) []uri.URI { include, _ := ParseIdent(file, ast.Includes(), name) if include != "" { resolver := view.Resolver() diff --git a/lsp/cache/bench_test.go b/store/bench_test.go similarity index 89% rename from lsp/cache/bench_test.go rename to store/bench_test.go index 22f5b20..f7cd745 100644 --- a/lsp/cache/bench_test.go +++ b/store/bench_test.go @@ -1,4 +1,4 @@ -package cache +package store import ( "context" @@ -6,30 +6,32 @@ import ( "testing" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/vfs" ) // benchChain builds a linear include chain: chain_0 includes chain_1, ... // Content lives in overlays, so the benchmark exercises store logic only — // no disk access. -func benchChain(b *testing.B, n int) (*View, []*FileChange) { +func benchChain(b *testing.B, n int) (*View, []*vfs.FileChange) { b.Helper() - files := make([]*FileChange, 0, n) + files := make([]*vfs.FileChange, 0, n) for i := range n { content := fmt.Sprintf("include \"chain_%d.thrift\"\n\nstruct S%d {\n\t1: required string Name\n}\n", i+1, i) if i == n-1 { content = fmt.Sprintf("struct S%d {\n\t1: required string Name\n}\n", i) } - files = append(files, &FileChange{ + files = append(files, &vfs.FileChange{ URI: uri.File(fmt.Sprintf("/tmp/chain_%d.thrift", i)), Version: 0, Content: []byte(content), - From: FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }) } - fs := NewOverlayFS(NewMemoizedFS()) + fs := vfs.NewOverlayFS(vfs.NewMemoizedFS()) if err := fs.Update(context.Background(), files); err != nil { b.Fatal(err) } diff --git a/lsp/cache/index.go b/store/index.go similarity index 98% rename from lsp/cache/index.go rename to store/index.go index 9ac98a5..13a5c68 100644 --- a/lsp/cache/index.go +++ b/store/index.go @@ -1,4 +1,4 @@ -package cache +package store import ( "fmt" @@ -171,7 +171,7 @@ func (w *indexWalker) visit(n syntax.Node) { case *syntax.Include, *syntax.CPPInclude: // Headers bind no names and hold no references. default: - panic(fmt.Sprintf("cache: indexWalker missing case for %T", n)) + panic(fmt.Sprintf("store: indexWalker missing case for %T", n)) } } diff --git a/lsp/cache/index_test.go b/store/index_test.go similarity index 99% rename from lsp/cache/index_test.go rename to store/index_test.go index 98a320f..85813c8 100644 --- a/lsp/cache/index_test.go +++ b/store/index_test.go @@ -1,4 +1,4 @@ -package cache +package store import ( "testing" diff --git a/lsp/cache/invalidation_test.go b/store/invalidation_test.go similarity index 80% rename from lsp/cache/invalidation_test.go rename to store/invalidation_test.go index 8bba229..5cf12c1 100644 --- a/lsp/cache/invalidation_test.go +++ b/store/invalidation_test.go @@ -1,4 +1,4 @@ -package cache +package store import ( "testing" @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/vfs" ) // The gundam test corpus: strike_rouge includes federation.gundam, which @@ -17,8 +19,8 @@ const ( char = "file:///tmp/char.thrift" ) -func gundamFiles() []*FileChange { - return []*FileChange{ +func gundamFiles() []*vfs.FileChange { + return []*vfs.FileChange{ { URI: uri.URI(strikeRouge), Content: []byte(`include "federation.gundam.thrift" @@ -26,7 +28,7 @@ func gundamFiles() []*FileChange { exception BayFull { 1: string message }`), - From: FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: uri.URI(federation), @@ -35,7 +37,7 @@ exception BayFull { struct Gundam { 1: required string Name }`), - From: FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, { URI: uri.URI(mobileSuit), @@ -44,7 +46,7 @@ struct Gundam { ZAKU_II, GELGOOG }`), - From: FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, } } @@ -53,14 +55,14 @@ struct Gundam { // first, then routed through View.FileChange. type viewHarness struct { view *View - fs *overlayFS + fs *vfs.OverlayFS } -func newViewHarness(t *testing.T, files []*FileChange) *viewHarness { +func newViewHarness(t *testing.T, files []*vfs.FileChange) *viewHarness { t.Helper() - c := NewMemoizedFS() - fs := NewOverlayFS(c) + c := vfs.NewMemoizedFS() + fs := vfs.NewOverlayFS(c) if err := fs.Update(t.Context(), files); err != nil { t.Fatal(err) @@ -79,10 +81,10 @@ func newViewHarness(t *testing.T, files []*FileChange) *viewHarness { // change applies a change like the server's didChange: overlay first, then // Update, returning the affected URIs. -func (h *viewHarness) change(t *testing.T, change *FileChange) []uri.URI { +func (h *viewHarness) change(t *testing.T, change *vfs.FileChange) []uri.URI { t.Helper() - if err := h.fs.Update(t.Context(), []*FileChange{change}); err != nil { + if err := h.fs.Update(t.Context(), []*vfs.FileChange{change}); err != nil { t.Fatal(err) } @@ -94,8 +96,8 @@ func (h *viewHarness) change(t *testing.T, change *FileChange) []uri.URI { func Test_FileChangeInvalidatesDependents(t *testing.T) { for _, tt := range []struct { name string - files []*FileChange - change *FileChange + files []*vfs.FileChange + change *vfs.FileChange wantAffected []uri.URI wantFresh map[uri.URI]string // changed files parse to this marker wantKept []uri.URI // unchanged files keep their original parse @@ -103,7 +105,7 @@ func Test_FileChangeInvalidatesDependents(t *testing.T) { { name: "change mid-chain invalidates transitive dependents", files: gundamFiles(), - change: &FileChange{ + change: &vfs.FileChange{ URI: uri.URI(federation), Version: 1, Content: []byte(`include "mobile_suit.zeon.thrift" @@ -112,7 +114,7 @@ struct Gundam { 1: required string Name, 2: optional i32 SerialNumber }`), - From: FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }, wantAffected: []uri.URI{federation, strikeRouge}, wantFresh: map[uri.URI]string{federation: "SerialNumber"}, @@ -121,7 +123,7 @@ struct Gundam { { name: "change leaf invalidates whole chain", files: gundamFiles(), - change: &FileChange{ + change: &vfs.FileChange{ URI: uri.URI(mobileSuit), Version: 1, Content: []byte(`enum ZeonForces { @@ -130,7 +132,7 @@ struct Gundam { GELGOOG, CHARS_ZAKU }`), - From: FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }, wantAffected: []uri.URI{mobileSuit, federation, strikeRouge}, wantFresh: map[uri.URI]string{mobileSuit: "CHARS_ZAKU"}, @@ -138,23 +140,23 @@ struct Gundam { }, { name: "change file with no dependents", - files: []*FileChange{ + files: []*vfs.FileChange{ { URI: uri.URI(char), Content: []byte(`struct Char { 1: optional string Title }`), - From: FileChangeTypeDidOpen, + From: vfs.FileChangeTypeDidOpen, }, }, - change: &FileChange{ + change: &vfs.FileChange{ URI: uri.URI(char), Version: 1, Content: []byte(`struct Char { 1: optional string Title, 2: optional bool Newtype }`), - From: FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }, wantAffected: []uri.URI{char}, wantFresh: map[uri.URI]string{char: "Newtype"}, diff --git a/lsp/cache/parse.go b/store/parse.go similarity index 96% rename from lsp/cache/parse.go rename to store/parse.go index 58f9c66..fcb3610 100644 --- a/lsp/cache/parse.go +++ b/store/parse.go @@ -1,4 +1,4 @@ -package cache +package store import ( "fmt" @@ -9,6 +9,7 @@ import ( "github.com/karitham/thrift-ls/lsp/mapper" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // collectTokens collects every identifier name in the document: definition @@ -30,7 +31,7 @@ func collectTokens(ast *syntax.Document, tokens map[string]struct{}) { } type ParsedFile struct { - fh FileHandle + fh vfs.FileHandle // ast is the latest available ast. The current fh content may not be // parsed, so ast may be nil when fh content is invalid. ast *syntax.Document @@ -120,7 +121,7 @@ func (p *ParsedFile) AggregatedError() error { } // Parse lexes and parses the file content into a ParsedFile. -func Parse(fh FileHandle) (*ParsedFile, error) { +func Parse(fh vfs.FileHandle) (*ParsedFile, error) { content, err := fh.Content() if err != nil { return nil, err diff --git a/lsp/cache/parse_test.go b/store/parse_test.go similarity index 84% rename from lsp/cache/parse_test.go rename to store/parse_test.go index d031b91..fa9d8cf 100644 --- a/lsp/cache/parse_test.go +++ b/store/parse_test.go @@ -1,21 +1,23 @@ -package cache +package store import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/karitham/thrift-ls/vfs" ) func TestParse(t *testing.T) { tests := []struct { name string - fh FileHandle + fh vfs.FileHandle assertion assert.ErrorAssertionFunc }{ { name: "normal", - fh: NewOverlay("file:///tmp/types.thrift", []byte(` + fh: vfs.NewOverlay("file:///tmp/types.thrift", []byte(` #include "base.thrift" struct Xtruct3 { @@ -29,7 +31,7 @@ struct Xtruct3 }, { name: "invalid ast", - fh: NewOverlay("file:///tmp/types.thrift", []byte(` + fh: vfs.NewOverlay("file:///tmp/types.thrift", []byte(` #include "base.thrift" struct Xtruct3 { @@ -55,7 +57,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 := BuildViewForTest([]*FileChange{ + ss := BuildViewForTest([]*vfs.FileChange{ {URI: "file:///tmp/test.thrift", Version: 0, Content: []byte(`struct S { 1: required string Name, } @@ -78,7 +80,7 @@ service Fed { } const i32 LIMIT = 1, -typedef string PilotName`), From: FileChangeTypeDidOpen}, +typedef string PilotName`), From: vfs.FileChangeTypeDidOpen}, }) pf, err := ss.Parse(t.Context(), "file:///tmp/test.thrift") diff --git a/lsp/cache/resolver_test.go b/store/resolver_test.go similarity index 95% rename from lsp/cache/resolver_test.go rename to store/resolver_test.go index 2e90844..3f200f6 100644 --- a/lsp/cache/resolver_test.go +++ b/store/resolver_test.go @@ -1,4 +1,4 @@ -package cache +package store import ( "path/filepath" @@ -9,6 +9,7 @@ import ( "github.com/karitham/thrift-ls/resolver/resolvertest" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) func TestResolver(t *testing.T) { @@ -20,7 +21,7 @@ func TestResolver(t *testing.T) { tree := resolvertest.Map{ sharedThrift: []byte("struct Shared {}"), } - fs := NewOverlayFS(NewMemFS(tree.URIs())) + fs := vfs.NewOverlayFS(vfs.NewMemFS(tree.URIs())) view := NewView(uri.File(root), fs, []string{sharedDir}) @@ -60,8 +61,8 @@ func TestResolver(t *testing.T) { // The include lives only in the editor overlay: an unsaved // buffer resolves even with no disk copy. overlayURI := uri.File(filepath.Join(sharedDir, "overlay.thrift")) - assert.NoError(t, fs.Update(t.Context(), []*FileChange{ - {URI: overlayURI, Version: 1, Content: []byte("struct Overlay {}"), From: FileChangeTypeDidOpen}, + assert.NoError(t, fs.Update(t.Context(), []*vfs.FileChange{ + {URI: overlayURI, Version: 1, Content: []byte("struct Overlay {}"), From: vfs.FileChangeTypeDidOpen}, })) t.Cleanup(func() { fs.Forget(overlayURI) }) diff --git a/lsp/cache/session.go b/store/session.go similarity index 87% rename from lsp/cache/session.go rename to store/session.go index 3fd03b0..9c7b5d2 100644 --- a/lsp/cache/session.go +++ b/store/session.go @@ -1,4 +1,4 @@ -package cache +package store import ( "context" @@ -6,28 +6,30 @@ import ( "sync" "go.lsp.dev/uri" + + "github.com/karitham/thrift-ls/vfs" ) type Session struct { // fs is the underlying file source (disk in production, in-memory in - // tests); the embedded overlayFS serves open-editor content over it. - fs FileSource + // tests); the embedded OverlayFS serves open-editor content over it. + fs vfs.FileSource viewMu sync.Mutex views []*View viewMap map[uri.URI]*View // map of URI->best view - // The session owns the overlayFS: open-editor content lives here, and + // The session owns the OverlayFS: open-editor content lives here, and // views read through it. - *overlayFS + *vfs.OverlayFS } -func NewSession(fs FileSource) *Session { +func NewSession(fs vfs.FileSource) *Session { sess := &Session{ fs: fs, views: make([]*View, 0), viewMap: make(map[uri.URI]*View), - overlayFS: NewOverlayFS(fs), + OverlayFS: vfs.NewOverlayFS(fs), } return sess @@ -46,7 +48,7 @@ func (s *Session) AddView(folder uri.URI, includePaths []string) *View { } } - view := NewView(folder, s.overlayFS, includePaths) + view := NewView(folder, s.OverlayFS, includePaths) s.views = append(s.views, view) clear(s.viewMap) @@ -128,5 +130,5 @@ func (s *Session) ViewOf(fileURI uri.URI) (*View, error) { } func (s *Session) WalkFiles(ctx context.Context, root uri.URI, fn func(uri.URI) error) error { - return s.overlayFS.WalkFiles(ctx, root, fn) + return s.OverlayFS.WalkFiles(ctx, root, fn) } diff --git a/lsp/cache/session_test.go b/store/session_test.go similarity index 92% rename from lsp/cache/session_test.go rename to store/session_test.go index 573bb89..e9e9fe5 100644 --- a/lsp/cache/session_test.go +++ b/store/session_test.go @@ -1,4 +1,4 @@ -package cache +package store import ( "testing" @@ -8,6 +8,7 @@ import ( "go.lsp.dev/uri" "github.com/karitham/thrift-ls/resolver/resolvertest" + "github.com/karitham/thrift-ls/vfs" ) func TestSessionViews(t *testing.T) { @@ -60,7 +61,7 @@ func TestSessionViews(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := NewSession(NewMemoizedFS()) + s := NewSession(vfs.NewMemoizedFS()) tt.setup(s) views := s.Views() @@ -76,7 +77,7 @@ func TestSessionViews(t *testing.T) { } func TestSessionAddViewDedups(t *testing.T) { - s := NewSession(NewMemoizedFS()) + s := NewSession(vfs.NewMemoizedFS()) folder := uri.File("/tmp/a") first := s.AddView(folder, nil) @@ -87,7 +88,7 @@ func TestSessionAddViewDedups(t *testing.T) { } func TestSessionRemoveViewForgetsMappings(t *testing.T) { - s := NewSession(NewMemoizedFS()) + s := NewSession(vfs.NewMemoizedFS()) folder := uri.File("/tmp/a") other := uri.File("/tmp/b") @@ -170,7 +171,7 @@ func TestSessionViewOf(t *testing.T) { setup: func(s *Session) { s.AddView(outer, nil) view := s.AddView(inner, nil) - view.Update(t.Context(), &FileChange{URI: externalFile, From: FileChangeTypeInitialize}) + view.Update(t.Context(), &vfs.FileChange{URI: externalFile, From: vfs.FileChangeTypeInitialize}) }, file: externalFile, want: inner, @@ -188,7 +189,7 @@ func TestSessionViewOf(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := NewSession(NewMemFS(resolvertest.Map{ + s := NewSession(vfs.NewMemFS(resolvertest.Map{ "/dependencies/shared.thrift": []byte("struct Shared {}"), }.URIs())) tt.setup(s) diff --git a/lsp/cache/snapshot.go b/store/snapshot.go similarity index 90% rename from lsp/cache/snapshot.go rename to store/snapshot.go index 82ce1ef..b8f816d 100644 --- a/lsp/cache/snapshot.go +++ b/store/snapshot.go @@ -1,4 +1,4 @@ -package cache +package store import ( "context" @@ -10,6 +10,7 @@ import ( "github.com/karitham/thrift-ls/resolver" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // Resolver provides centralized include path resolution. @@ -21,17 +22,17 @@ type Resolver struct { // newResolver builds a Resolver resolving against src: open files by their // overlay presence, the rest through cheap existence checks. No content is // read to test existence. -func newResolver(includePaths []string, src FileSource) *Resolver { +func newResolver(includePaths []string, src vfs.FileSource) *Resolver { return &Resolver{ includePaths: includePaths, central: resolver.New(includePaths, resolver.WithChecker(fileChecker{src: src})), } } -// fileChecker translates the resolver's string paths to URIs at the cache +// fileChecker translates the resolver's string paths to URIs at the store boundary // boundary. Known sources answer cheaply; unknown ones fall back to a read. type fileChecker struct { - src FileSource + src vfs.FileSource } func (c fileChecker) Exists(ctx context.Context, path string) bool { @@ -39,7 +40,7 @@ func (c fileChecker) Exists(ctx context.Context, path string) bool { return false } - if ex, ok := c.src.(Checker); ok { + if ex, ok := c.src.(vfs.Checker); ok { return ex.Exists(ctx, path) } @@ -126,15 +127,15 @@ func getIncludeNameFromPath(path string) string { return strings.TrimSuffix(name, ".thrift") } -func BuildViewForTest(files []*FileChange) *View { +func BuildViewForTest(files []*vfs.FileChange) *View { return BuildViewForTestWithPaths(nil, files) } // BuildViewForTestWithPaths is BuildViewForTest with configured include // paths, for cross-project include resolution tests. -func BuildViewForTestWithPaths(includePaths []string, files []*FileChange) *View { - c := NewMemoizedFS() - fs := NewOverlayFS(c) +func BuildViewForTestWithPaths(includePaths []string, files []*vfs.FileChange) *View { + c := vfs.NewMemoizedFS() + fs := vfs.NewOverlayFS(c) _ = fs.Update(context.TODO(), files) view := NewView("file:///tmp", fs, includePaths) diff --git a/lsp/cache/view.go b/store/view.go similarity index 96% rename from lsp/cache/view.go rename to store/view.go index dc3b67b..40c4562 100644 --- a/lsp/cache/view.go +++ b/store/view.go @@ -1,4 +1,4 @@ -package cache +package store import ( "context" @@ -11,13 +11,14 @@ import ( "go.lsp.dev/uri" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // viewEntry is one file's store record: its content handle and the parse // derived from it. Entries are replaced wholesale when content changes, so // a live entry never goes stale. type viewEntry struct { - fh FileHandle + fh vfs.FileHandle parsed *ParsedFile // nil until parsed } @@ -33,7 +34,7 @@ type View struct { // directory in single-file mode. folder uri.URI - fs FileSource + fs vfs.FileSource includePaths []string @@ -59,7 +60,7 @@ func generationOf(ctx context.Context) (uint64, bool) { return generation, ok } -func NewView(folder uri.URI, fs FileSource, includePaths []string) *View { +func NewView(folder uri.URI, fs vfs.FileSource, includePaths []string) *View { return &View{ folder: folder, fs: fs, @@ -100,7 +101,7 @@ func (v *View) WalkFiles(ctx context.Context, root uri.URI, fn func(uri.URI) err // ReadFile returns the current content of uri: the editor overlay for open // files, the memoized disk content otherwise. Content is not cached on the // view — the file sources already memoize it. -func (v *View) ReadFile(ctx context.Context, u uri.URI) (FileHandle, error) { +func (v *View) ReadFile(ctx context.Context, u uri.URI) (vfs.FileHandle, error) { slog.Debug("view read file", "uri", u) return v.fs.ReadFile(ctx, u) @@ -316,7 +317,7 @@ func (v *View) KnownFiles() []uri.URI { } // Generation returns the view's change counter. It increments on every -// FileChange; asynchronous work captures it and re-checks before publishing. +// vfs.FileChange; asynchronous work captures it and re-checks before publishing. func (v *View) Generation() uint64 { return v.gen.Load() } @@ -379,7 +380,7 @@ type ChangeResult struct { // entries and re-parses them, so their include edges are fresh before any // request observes the change. It returns what changed; publishing derived // results (diagnostics) is the caller's policy. -func (v *View) Update(ctx context.Context, changes ...*FileChange) ChangeResult { +func (v *View) Update(ctx context.Context, changes ...*vfs.FileChange) ChangeResult { uris := make([]uri.URI, 0, len(changes)) for _, change := range changes { uris = append(uris, change.URI) diff --git a/lsp/cache/view_test.go b/store/view_test.go similarity index 93% rename from lsp/cache/view_test.go rename to store/view_test.go index 238c448..f4ebae4 100644 --- a/lsp/cache/view_test.go +++ b/store/view_test.go @@ -1,4 +1,4 @@ -package cache +package store import ( "context" @@ -9,6 +9,7 @@ import ( "go.lsp.dev/uri" "github.com/karitham/thrift-ls/syntax" + "github.com/karitham/thrift-ls/vfs" ) // gundam-themed URIs, fixed for deterministic sort order across tests: @@ -162,14 +163,14 @@ func Test_View_GenerationAndIsCurrent(t *testing.T) { before := h.view.Generation() assert.True(t, h.view.IsCurrent(before)) - h.change(t, &FileChange{ + h.change(t, &vfs.FileChange{ URI: uri.URI(federation), Version: 1, Content: []byte(`include "mobile_suit.zeon.thrift"`), - From: FileChangeTypeDidChange, + From: vfs.FileChangeTypeDidChange, }) - assert.Greater(t, h.view.Generation(), before, "FileChange bumps the generation") + assert.Greater(t, h.view.Generation(), before, "vfs.FileChange bumps the generation") 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())) } @@ -183,10 +184,10 @@ func Test_ViewParseIncludeCycles(t *testing.T) { amuro := uri.File(filepath.Join(dir, "amuro.thrift")) self := uri.File(filepath.Join(dir, "side_effect.thrift")) - files := []*FileChange{ - {URI: char, Content: []byte(`include "amuro.thrift"`), From: FileChangeTypeDidOpen}, - {URI: amuro, Content: []byte(`include "char.thrift"`), From: FileChangeTypeDidOpen}, - {URI: self, Content: []byte(`include "side_effect.thrift"`), From: FileChangeTypeDidOpen}, + files := []*vfs.FileChange{ + {URI: char, Content: []byte(`include "amuro.thrift"`), From: vfs.FileChangeTypeDidOpen}, + {URI: amuro, Content: []byte(`include "char.thrift"`), From: vfs.FileChangeTypeDidOpen}, + {URI: self, Content: []byte(`include "side_effect.thrift"`), From: vfs.FileChangeTypeDidOpen}, } ss := BuildViewForTest(files)