From e3c10757312b25b51f48292800c5765a81ab0d44 Mon Sep 17 00:00:00 2001 From: Aofei Sheng Date: Fri, 27 Dec 2024 10:54:04 +0800 Subject: [PATCH] feat(tools/spxls): introduce compile cache (#1176) Signed-off-by: Aofei Sheng --- .../editor/code-editor/lsp/index.ts | 5 +- spx-gui/src/models/project/index.ts | 14 +- tools/spxls/index.d.ts | 1 + tools/spxls/internal/server/command_test.go | 27 +-- tools/spxls/internal/server/compile.go | 70 +++++- .../spxls/internal/server/completion_test.go | 11 +- .../spxls/internal/server/definition_test.go | 49 ++-- .../spxls/internal/server/diagnostic_test.go | 97 +++----- tools/spxls/internal/server/document_test.go | 31 +-- tools/spxls/internal/server/format_test.go | 35 +-- tools/spxls/internal/server/highlight_test.go | 11 +- tools/spxls/internal/server/hover_test.go | 19 +- .../internal/server/implementation_test.go | 19 +- tools/spxls/internal/server/reference_test.go | 17 +- tools/spxls/internal/server/rename_test.go | 213 +++++++----------- .../internal/server/semantic_token_test.go | 11 +- tools/spxls/internal/server/server.go | 5 +- tools/spxls/internal/server/server_test.go | 13 ++ tools/spxls/internal/server/signature_test.go | 11 +- tools/spxls/internal/vfs/mapfs.go | 53 +++-- tools/spxls/internal/vfs/mapfs_test.go | 24 +- tools/spxls/main.go | 12 +- 22 files changed, 362 insertions(+), 386 deletions(-) create mode 100644 tools/spxls/internal/server/server_test.go diff --git a/spx-gui/src/components/editor/code-editor/lsp/index.ts b/spx-gui/src/components/editor/code-editor/lsp/index.ts index 568257a87..b21eb6c73 100644 --- a/spx-gui/src/components/editor/code-editor/lsp/index.ts +++ b/spx-gui/src/components/editor/code-editor/lsp/index.ts @@ -62,7 +62,10 @@ export class SpxLSPClient extends Disposable { if (['.spx', '.json'].includes(ext)) { // Only `.spx` & `.json` files are needed for `spxls` const ab = await file.arrayBuffer() - loadedFiles[path] = { content: new Uint8Array(ab) } + loadedFiles[path] = { + content: new Uint8Array(ab), + modTime: this.project.modTime ?? Date.now() + } debugFiles[path] = await toText(file) } }) diff --git a/spx-gui/src/models/project/index.ts b/spx-gui/src/models/project/index.ts index 320b1e151..f4cdb9bf5 100644 --- a/spx-gui/src/models/project/index.ts +++ b/spx-gui/src/models/project/index.ts @@ -104,7 +104,7 @@ export class Project extends Disposable { releaseCount?: number remixCount?: number - /** Files' hash of game content, available when project under editing */ + /** Files' hash of game content, available when project is under editing */ filesHash?: string private lastSyncedFilesHash?: string /** If there is any change of game content not synced (to cloud) yet. */ @@ -114,6 +114,9 @@ export class Project extends Disposable { return this.lastSyncedFilesHash !== this.filesHash } + /** Modification time in milliseconds of project state, available when project is under editing */ + modTime?: number + stage: Stage sprites: Sprite[] sounds: Sound[] @@ -553,7 +556,12 @@ export class Project extends Disposable { ) } - /** watch for all changes, auto save to local cache, or touch all files to trigger lazy loading to ensure they are in memory */ + /** + * Watch for all changes to: + * 1. Auto save to local cache when enabled. + * 2. Touch all files to trigger lazy loading when not in local cache mode. + * 3. Update modification time. + */ private autoSaveToLocalCache: (() => void) | null = null private startAutoSaveToLocalCache(localCacheKey: string) { const saveToLocalCache = debounce(() => this.saveToLocalCache(localCacheKey), 1000) @@ -569,6 +577,7 @@ export class Project extends Disposable { this.autoSaveToLocalCache = () => { if (this.autoSaveMode === AutoSaveMode.LocalCache) saveToLocalCache() else touchFiles() + this.modTime = Date.now() } this.addDisposer( @@ -588,6 +597,7 @@ export class Project extends Disposable { if (this.lastSyncedFilesHash == null) { this.lastSyncedFilesHash = this.filesHash } + this.modTime = Date.now() this.startAutoSaveToCloud(localCacheKey) this.startAutoSaveToLocalCache(localCacheKey) diff --git a/tools/spxls/index.d.ts b/tools/spxls/index.d.ts index 4429d6200..a3921315e 100644 --- a/tools/spxls/index.d.ts +++ b/tools/spxls/index.d.ts @@ -129,4 +129,5 @@ export type Files = { */ export type File = { content: Uint8Array + modTime: number // unix timestamp in milliseconds } diff --git a/tools/spxls/internal/server/command_test.go b/tools/spxls/internal/server/command_test.go index 3db597181..26eaa58b3 100644 --- a/tools/spxls/internal/server/command_test.go +++ b/tools/spxls/internal/server/command_test.go @@ -5,29 +5,26 @@ import ( "testing" "github.com/goplus/builder/tools/spxls/internal/util" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerSpxGetDefinitions(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) mainSpxFileScopeParams := []SpxGetDefinitionsParams{ @@ -143,14 +140,12 @@ onStart => { }) t.Run("ParseError", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` // Invalid syntax var ( MySprite Sprite `), - } }), nil) mainSpxFileScopeParams := []SpxGetDefinitionsParams{ @@ -183,24 +178,22 @@ var ( }) t.Run("TrailingEmptyLinesOfSpriteCode", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) mainSpxFileScopeParams := []SpxGetDefinitionsParams{ diff --git a/tools/spxls/internal/server/compile.go b/tools/spxls/internal/server/compile.go index 5c0621be5..252313c46 100644 --- a/tools/spxls/internal/server/compile.go +++ b/tools/spxls/internal/server/compile.go @@ -5,9 +5,11 @@ import ( "fmt" "go/types" "io/fs" + "maps" "path" "slices" "strings" + "time" "github.com/goplus/builder/tools/spxls/internal/util" "github.com/goplus/builder/tools/spxls/internal/vfs" @@ -302,16 +304,77 @@ func (r *compileResult) addDiagnostics(documentURI DocumentURI, diags ...Diagnos r.diagnostics[documentURI] = append(r.diagnostics[documentURI], dedupedDiags...) } +// compileCache represents a cache for compilation results. +type compileCache struct { + result *compileResult + spxFileModTimes map[string]time.Time +} + // compile compiles spx source files and returns compile result. -// -// TODO: Move diagnostics from [compileResult] to error return value by using -// [errors.Join]. func (s *Server) compile() (*compileResult, error) { spxFiles, err := s.spxFiles() if err != nil { return nil, fmt.Errorf("failed to get spx files: %w", err) } + if len(spxFiles) == 0 { + return nil, errNoValidSpxFiles + } + slices.Sort(spxFiles) + + s.compileCacheMu.Lock() + defer s.compileCacheMu.Unlock() + + // Try to use cache first. + if cache := s.lastCompileCache; cache != nil { + // Check if spx file set has changed. + cachedSpxFiles := slices.Sorted(maps.Keys(cache.spxFileModTimes)) + if slices.Equal(spxFiles, cachedSpxFiles) { + // Check if any spx file has been modified. + modified := false + for _, spxFile := range spxFiles { + fi, err := fs.Stat(s.workspaceRootFS, spxFile) + if err != nil { + return nil, fmt.Errorf("failed to stat file %q: %w", spxFile, err) + } + if cachedModTime, ok := cache.spxFileModTimes[spxFile]; !ok || !fi.ModTime().Equal(cachedModTime) { + modified = true + break + } + } + if !modified { + return cache.result, nil + } + } + } + // Compile uncached if cache is not used. + result, err := s.compileUncached(spxFiles) + if err != nil { + return nil, err + } + + // Update cache. + modTimes := make(map[string]time.Time, len(spxFiles)) + for _, spxFile := range spxFiles { + fi, err := fs.Stat(s.workspaceRootFS, spxFile) + if err != nil { + return nil, fmt.Errorf("failed to stat file %q: %w", spxFile, err) + } + modTimes[spxFile] = fi.ModTime() + } + s.lastCompileCache = &compileCache{ + result: result, + spxFileModTimes: modTimes, + } + + return result, nil +} + +// compileUncached compiles spx source files without using cache. +// +// TODO: Move diagnostics from [compileResult] to error return value by using +// [errors.Join]. +func (s *Server) compileUncached(spxFiles []string) (*compileResult, error) { result := &compileResult{ fset: goptoken.NewFileSet(), mainPkg: types.NewPackage("main", "main"), @@ -419,6 +482,7 @@ func (s *Server) compile() (*compileResult, error) { return result, nil } + var err error result.spxPkg, err = s.importer.Import(spxPkgPath) if err != nil { return nil, fmt.Errorf("failed to import spx package: %w", err) diff --git a/tools/spxls/internal/server/completion_test.go b/tools/spxls/internal/server/completion_test.go index 00fa1a316..f910882f6 100644 --- a/tools/spxls/internal/server/completion_test.go +++ b/tools/spxls/internal/server/completion_test.go @@ -4,16 +4,14 @@ import ( "testing" "github.com/goplus/builder/tools/spxls/internal/util" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentCompletion(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) @@ -21,13 +19,12 @@ var ( MySprite. run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) emptyLineItems, err := s.textDocumentCompletion(&CompletionParams{ diff --git a/tools/spxls/internal/server/definition_test.go b/tools/spxls/internal/server/definition_test.go index e76b2c822..722dd4aa0 100644 --- a/tools/spxls/internal/server/definition_test.go +++ b/tools/spxls/internal/server/definition_test.go @@ -3,29 +3,26 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentDefinition(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) mainSpxMySpriteDef, err := s.textDocumentDefinition(&DefinitionParams{ @@ -73,12 +70,10 @@ onStart => { }) t.Run("BuiltinType", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var x int `), - } }), nil) def, err := s.textDocumentDefinition(&DefinitionParams{ @@ -92,12 +87,10 @@ var x int }) t.Run("InvalidPosition", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var x int `), - } }), nil) def, err := s.textDocumentDefinition(&DefinitionParams{ @@ -113,15 +106,13 @@ var x int func TestServerTextDocumentTypeDefinition(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` type MyType struct { field int } var x MyType `), - } }), nil) def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{ @@ -143,15 +134,13 @@ var x MyType }) t.Run("SpriteType", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{ @@ -165,12 +154,10 @@ var ( }) t.Run("BuiltinType", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var x int `), - } }), nil) def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{ @@ -184,12 +171,10 @@ var x int }) t.Run("InvalidPosition", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var x int `), - } }), nil) def, err := s.textDocumentTypeDefinition(&TypeDefinitionParams{ diff --git a/tools/spxls/internal/server/diagnostic_test.go b/tools/spxls/internal/server/diagnostic_test.go index 6250defc1..a51867dc1 100644 --- a/tools/spxls/internal/server/diagnostic_test.go +++ b/tools/spxls/internal/server/diagnostic_test.go @@ -3,7 +3,6 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -53,9 +52,7 @@ onCloned => { func TestServerTextDocumentDiagnostic(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return newTestFileMap() - }), nil) + s := New(newMapFSWithoutModTime(newTestFileMap()), nil) params := &DocumentDiagnosticParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, } @@ -71,15 +68,13 @@ func TestServerTextDocumentDiagnostic(t *testing.T) { }) t.Run("ParseError", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - fileMap := newTestFileMap() - fileMap["main.spx"] = []byte(` + fileMap := newTestFileMap() + fileMap["main.spx"] = []byte(` // Invalid syntax, missing closing parenthesis var ( MyAircraft MyAircraft `) - return fileMap - }), nil) + s := New(newMapFSWithoutModTime(fileMap), nil) params := &DocumentDiagnosticParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, } @@ -111,11 +106,9 @@ var ( }) t.Run("NonSpxFile", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - fileMap := newTestFileMap() - fileMap["main.gop"] = []byte(`echo "Hello, Go+!"`) - return fileMap - }), nil) + fileMap := newTestFileMap() + fileMap["main.gop"] = []byte(`echo "Hello, Go+!"`) + s := New(newMapFSWithoutModTime(fileMap), nil) params := &DocumentDiagnosticParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.gop"}, } @@ -131,9 +124,7 @@ var ( }) t.Run("FileNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return newTestFileMap() - }), nil) + s := New(newMapFSWithoutModTime(newTestFileMap()), nil) params := &DocumentDiagnosticParams{ TextDocument: TextDocumentIdentifier{URI: "file:///notexist.spx"}, } @@ -151,9 +142,7 @@ var ( func TestServerWorkspaceDiagnostic(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return newTestFileMap() - }), nil) + s := New(newMapFSWithoutModTime(newTestFileMap()), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) require.NoError(t, err) @@ -174,15 +163,13 @@ func TestServerWorkspaceDiagnostic(t *testing.T) { }) t.Run("ParseError", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` // Invalid syntax, missing closing parenthesis var ( MyAircraft MyAircraft `), - "MyAircraft.spx": []byte(`var x int`), - } + "MyAircraft.spx": []byte(`var x int`), }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) @@ -224,9 +211,7 @@ var ( }) t.Run("EmptyWorkspace", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{} - }), nil) + s := New(newMapFSWithoutModTime(map[string][]byte{}), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) require.EqualError(t, err, "no valid spx files found in main package") @@ -234,9 +219,8 @@ var ( }) t.Run("SoundResourceNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( AutoBindingSoundName Sound ) @@ -246,7 +230,7 @@ var ( ) run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` const ConstSoundName = "ConstSoundName" var ( VarSoundName string @@ -262,7 +246,6 @@ onStart => { play AutoBindingSoundName2 } `), - } }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) @@ -356,14 +339,13 @@ onStart => { }) t.Run("BackdropResourceNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` onBackdrop "", func() {} onBackdrop "NonExistentBackdrop", func() {} run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` const ConstBackdropName = "ConstBackdropName" var VarBackdropName string VarBackdropName = "VarBackdropName" @@ -373,7 +355,6 @@ onStart => { onBackdrop VarBackdropName, func() {} } `), - } }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) @@ -427,9 +408,8 @@ onStart => { }) t.Run("SpriteResourceNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite1 Sprite MySprite2 MySprite2 @@ -437,21 +417,20 @@ var ( ) run "assets", {Title: "My Game"} `), - "MySprite1.spx": []byte(` + "MySprite1.spx": []byte(` var MySprite3 Sprite onStart => { animate "roll-in" MySprite2.animate "roll-out" } `), - "MySprite2.spx": []byte(` + "MySprite2.spx": []byte(` onStart => { MySprite1.animate "roll-out" animate "roll-in" MySprite2.animate "roll-out" } `), - } }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) @@ -547,19 +526,17 @@ onStart => { }) t.Run("SpriteCostumeResourceNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { setCostume "" setCostume "NonExistentCostume" } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) @@ -595,19 +572,17 @@ onStart => { }) t.Run("SpriteAnimationResourceNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { animate "" animate "roll-in" } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) @@ -643,12 +618,11 @@ onStart => { }) t.Run("WidgetResourceNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` const ConstWidgetName = "ConstWidgetName" var VarWidgetName string VarWidgetName = "VarWidgetName" @@ -659,8 +633,7 @@ onStart => { getWidget Monitor, VarWidgetName } `), - "assets/index.json": []byte(`{}`), - } + "assets/index.json": []byte(`{}`), }), nil) report, err := s.workspaceDiagnostic(&WorkspaceDiagnosticParams{}) diff --git a/tools/spxls/internal/server/document_test.go b/tools/spxls/internal/server/document_test.go index 2a02a7574..9cdab9ee6 100644 --- a/tools/spxls/internal/server/document_test.go +++ b/tools/spxls/internal/server/document_test.go @@ -3,23 +3,21 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentDocumentLink(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySound Sound MySprite Sprite ) run "assets", {Title: "Bullet (by Go+)"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { play "sound1" onBackdrop "backdrop1", func() {} @@ -28,10 +26,9 @@ onStart => { getWidget Monitor, "widget1" } `), - "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1"}],"zorder":[{"name":"widget1","type":"monitor"}]}`), - "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}],"fAnimations":{"anim1":{}}}`), - "assets/sounds/sound1/index.json": []byte(`{}`), - } + "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1"}],"zorder":[{"name":"widget1","type":"monitor"}]}`), + "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}],"fAnimations":{"anim1":{}}}`), + "assets/sounds/sound1/index.json": []byte(`{}`), }), nil) paramsForMainSpx := &DocumentLinkParams{ @@ -189,10 +186,8 @@ onStart => { }) t.Run("NonSpxFile", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.gop": []byte(`echo "Hello, Go+!"`), - } + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.gop": []byte(`echo "Hello, Go+!"`), }), nil) params := &DocumentLinkParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.gop"}, @@ -204,9 +199,7 @@ onStart => { }) t.Run("FileNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{} - }), nil) + s := New(newMapFSWithoutModTime(map[string][]byte{}), nil) params := &DocumentLinkParams{ TextDocument: TextDocumentIdentifier{URI: "file:///notexist.spx"}, } @@ -217,14 +210,12 @@ onStart => { }) t.Run("ParseError", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` // Invalid syntax var ( MySound Sound `), - } }), nil) params := &DocumentLinkParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, diff --git a/tools/spxls/internal/server/format_test.go b/tools/spxls/internal/server/format_test.go index 8117cabdb..1839313e8 100644 --- a/tools/spxls/internal/server/format_test.go +++ b/tools/spxls/internal/server/format_test.go @@ -3,16 +3,14 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentFormatting(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` // A spx game. var ( @@ -21,7 +19,6 @@ var ( ) run "assets", { Title: "Bullet (by Go+)" } `), - } }), nil) params := &DocumentFormattingParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, @@ -48,10 +45,8 @@ run "assets", {Title: "Bullet (by Go+)"} }) t.Run("NonSpxFile", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.gop": []byte(`echo "Hello, Go+!"`), - } + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.gop": []byte(`echo "Hello, Go+!"`), }), nil) params := &DocumentFormattingParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.gop"}, @@ -63,9 +58,7 @@ run "assets", {Title: "Bullet (by Go+)"} }) t.Run("FileNotFound", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return nil - }), nil) + s := New(newMapFSWithoutModTime(map[string][]byte{}), nil) params := &DocumentFormattingParams{ TextDocument: TextDocumentIdentifier{URI: "file:///notexist.spx"}, } @@ -76,10 +69,8 @@ run "assets", {Title: "Bullet (by Go+)"} }) t.Run("NoChangesNeeded", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(`run "assets", {Title: "Bullet (by Go+)"}` + "\n"), - } + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(`run "assets", {Title: "Bullet (by Go+)"}` + "\n"), }), nil) params := &DocumentFormattingParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, @@ -91,10 +82,8 @@ run "assets", {Title: "Bullet (by Go+)"} }) t.Run("FormatError", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte("vbr Foobar string"), - } + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte("vbr Foobar string"), }), nil) params := &DocumentFormattingParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, @@ -107,9 +96,8 @@ run "assets", {Title: "Bullet (by Go+)"} }) t.Run("WithFormatSpx", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(`// A spx game. + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(`// A spx game. var ( // The aircraft. @@ -139,7 +127,6 @@ var ( Bullet6 Bullet // The sixth bullet. ) `), - } }), nil) params := &DocumentFormattingParams{ TextDocument: TextDocumentIdentifier{URI: "file:///main.spx"}, diff --git a/tools/spxls/internal/server/highlight_test.go b/tools/spxls/internal/server/highlight_test.go index 46d67b457..1c6d5a5b6 100644 --- a/tools/spxls/internal/server/highlight_test.go +++ b/tools/spxls/internal/server/highlight_test.go @@ -3,29 +3,26 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentDocumentHighlight(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) mySpriteHighlights, err := s.textDocumentDocumentHighlight(&DocumentHighlightParams{ diff --git a/tools/spxls/internal/server/hover_test.go b/tools/spxls/internal/server/hover_test.go index b671d75b9..3def425f2 100644 --- a/tools/spxls/internal/server/hover_test.go +++ b/tools/spxls/internal/server/hover_test.go @@ -3,16 +3,14 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentHover(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` import ( "fmt" "image" @@ -55,7 +53,7 @@ onClick => {} on "MySprite" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` MySprite.onClick => {} onClick => {} onStart => { @@ -64,9 +62,8 @@ onStart => { imagePoint.X = 100 } `), - "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}]}`), - "assets/sounds/MySound/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}]}`), + "assets/sounds/MySound/index.json": []byte(`{}`), }), nil) mySoundHover, err := s.textDocumentHover(&HoverParams{ @@ -447,10 +444,8 @@ onStart => { }) t.Run("InvalidPosition", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(`var x int`), - } + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(`var x int`), }), nil) hover, err := s.textDocumentHover(&HoverParams{ diff --git a/tools/spxls/internal/server/implementation_test.go b/tools/spxls/internal/server/implementation_test.go index 41494c683..9a5906bdb 100644 --- a/tools/spxls/internal/server/implementation_test.go +++ b/tools/spxls/internal/server/implementation_test.go @@ -3,16 +3,14 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentImplementation(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` type MyInterface interface { myMethod() } @@ -27,7 +25,6 @@ func (t MyType2) myMethod() {} var x MyInterface `), - } }), nil) implementations, err := s.textDocumentImplementation(&ImplementationParams{ @@ -58,14 +55,12 @@ var x MyInterface }) t.Run("NonInterfaceMethod", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` type MyType struct{} func (t MyType) myMethod() {} `), - } }), nil) implementation, err := s.textDocumentImplementation(&ImplementationParams{ @@ -88,12 +83,10 @@ func (t MyType) myMethod() {} }) t.Run("InvalidPosition", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` type MyType struct{} `), - } }), nil) implementation, err := s.textDocumentImplementation(&ImplementationParams{ diff --git a/tools/spxls/internal/server/reference_test.go b/tools/spxls/internal/server/reference_test.go index 24821a3ff..a5828ce44 100644 --- a/tools/spxls/internal/server/reference_test.go +++ b/tools/spxls/internal/server/reference_test.go @@ -3,29 +3,26 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentReferences(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) mainSpxMySpriteDef, err := s.textDocumentReferences(&ReferenceParams{ @@ -91,10 +88,8 @@ onStart => { }) t.Run("InvalidPosition", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(`var x int`), - } + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(`var x int`), }), nil) refs, err := s.textDocumentReferences(&ReferenceParams{ diff --git a/tools/spxls/internal/server/rename_test.go b/tools/spxls/internal/server/rename_test.go index 702cfbf6e..491f33f7e 100644 --- a/tools/spxls/internal/server/rename_test.go +++ b/tools/spxls/internal/server/rename_test.go @@ -3,29 +3,26 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentPrepareRename(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) range1, err := s.textDocumentPrepareRename(&PrepareRenameParams{ @@ -67,9 +64,8 @@ onStart => { func TestServerTextDocumentRename(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) @@ -77,14 +73,13 @@ const Foo = "bar" MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` println Foo onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) workspaceEdit, err := s.textDocumentRename(&RenameParams{ @@ -118,9 +113,8 @@ onStart => { }) t.Run("Rename reference", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) @@ -128,14 +122,13 @@ const Foo = "bar" MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` println Foo onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) workspaceEdit, err := s.textDocumentRename(&RenameParams{ @@ -169,22 +162,20 @@ onStart => { }) t.Run("SpxResource", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) workspaceEdit, err := s.textDocumentRename(&RenameParams{ @@ -227,19 +218,17 @@ onStart => { func TestServerSpxRenameBackdropResource(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` onBackdrop "backdrop1", func() {} run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { onBackdrop "backdrop1", func() {} } `), - "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"}]}`), - } + "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -274,20 +263,18 @@ onStart => { }) t.Run("ConstantName", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` const Backdrop1 = "backdrop1" onBackdrop Backdrop1, func() {} run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { onBackdrop Backdrop1, func() {} } `), - "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"}]}`), - } + "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -315,20 +302,18 @@ onStart => { }) t.Run("TypedConstantName", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` // const Backdrop1 BackdropName = "backdrop1" // TODO: See https://github.com/goplus/builder/issues/1127 onBackdrop "backdrop1", func() {} run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { onBackdrop "backdrop1", func() {} } `), - "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"}]}`), - } + "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -372,14 +357,12 @@ onStart => { }) t.Run("AlreadyExists", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` onBackdrop "backdrop1", func() {} run "assets", {Title: "My Game"} `), - "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"},{"name":"backdrop2","path":"backdrop2.png"}]}`), - } + "assets/index.json": []byte(`{"backdrops":[{"name":"backdrop1","path":"backdrop1.png"},{"name":"backdrop2","path":"backdrop2.png"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -396,22 +379,20 @@ run "assets", {Title: "My Game"} func TestServerSpxRenameSoundResource(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( Sound1 Sound ) play "Sound1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { play Sound1 } `), - "assets/sounds/Sound1/index.json": []byte(`{"path":"sound1.wav"}`), - } + "assets/sounds/Sound1/index.json": []byte(`{"path":"sound1.wav"}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -453,15 +434,13 @@ onStart => { }) t.Run("AlreadyExists", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` play "Sound1" run "assets", {Title: "My Game"} `), - "assets/sounds/Sound1/index.json": []byte(`{"path":"sound1.wav"}`), - "assets/sounds/Sound2/index.json": []byte(`{"path":"sound2.wav"}`), - } + "assets/sounds/Sound1/index.json": []byte(`{"path":"sound1.wav"}`), + "assets/sounds/Sound2/index.json": []byte(`{"path":"sound2.wav"}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -478,22 +457,20 @@ run "assets", {Title: "My Game"} func TestServerSpxRenameSpriteResource(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( Sprite1 Sprite ) Sprite1.turn Left run "assets", {Title: "My Game"} `), - "Sprite1.spx": []byte(` + "Sprite1.spx": []byte(` onStart => { Sprite1.turn Right } `), - "assets/sprites/Sprite1/index.json": []byte(`{}`), - } + "assets/sprites/Sprite1/index.json": []byte(`{}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -535,22 +512,20 @@ onStart => { }) t.Run("SpriteType", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( Sprite1 Sprite1 ) Sprite1.turn Left run "assets", {Title: "My Game"} `), - "Sprite1.spx": []byte(` + "Sprite1.spx": []byte(` onStart => { Sprite1.turn Right } `), - "assets/sprites/Sprite1/index.json": []byte(`{}`), - } + "assets/sprites/Sprite1/index.json": []byte(`{}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -599,9 +574,8 @@ onStart => { }) t.Run("AlreadyExists", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( Sprite1 Sprite Sprite2 Sprite @@ -610,19 +584,18 @@ Sprite1.turn Left Sprite2.turn Left run "assets", {Title: "My Game"} `), - "Sprite1.spx": []byte(` + "Sprite1.spx": []byte(` onStart => { Sprite1.turn Right } `), - "Sprite2.spx": []byte(` + "Sprite2.spx": []byte(` onStart => { Sprite2.turn Right } `), - "assets/sprites/Sprite1/index.json": []byte(`{}`), - "assets/sprites/Sprite2/index.json": []byte(`{}`), - } + "assets/sprites/Sprite1/index.json": []byte(`{}`), + "assets/sprites/Sprite2/index.json": []byte(`{}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -639,22 +612,20 @@ onStart => { func TestServerSpxRenameSpriteCostumeResource(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.setCostume "costume1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { setCostume "costume1" } `), - "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}]}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -689,22 +660,20 @@ onStart => { }) t.Run("AlreadyExists", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.setCostume "costume1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { setCostume "costume1" } `), - "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"},{"name":"costume2"}]}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"},{"name":"costume2"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -719,22 +688,20 @@ onStart => { }) t.Run("NonExistentSprite", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.setCostume "costume1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { setCostume "costume1" } `), - "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}]}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"costumes":[{"name":"costume1"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -751,22 +718,20 @@ onStart => { func TestServerSpxRenameSpriteAnimationResource(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.animate "anim1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { animate "anim1" } `), - "assets/sprites/MySprite/index.json": []byte(`{"fAnimations":{"anim1":{}}}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"fAnimations":{"anim1":{}}}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -801,22 +766,20 @@ onStart => { }) t.Run("AlreadyExists", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.animate "anim1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { animate "anim1" } `), - "assets/sprites/MySprite/index.json": []byte(`{"fAnimations":{"anim1":{},"anim2":{}}}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"fAnimations":{"anim1":{},"anim2":{}}}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -831,22 +794,20 @@ onStart => { }) t.Run("NonExistentSprite", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.animate "anim1" run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { animate "anim1" } `), - "assets/sprites/MySprite/index.json": []byte(`{"fAnimations":{"anim1":{}}}`), - } + "assets/sprites/MySprite/index.json": []byte(`{"fAnimations":{"anim1":{}}}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -863,18 +824,16 @@ onStart => { func TestServerSpxRenameWidgetResource(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { getWidget Monitor, "widget1" } `), - "assets/index.json": []byte(`{"zorder":[{"name":"widget1"}]}`), - } + "assets/index.json": []byte(`{"zorder":[{"name":"widget1"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) @@ -899,18 +858,16 @@ onStart => { }) t.Run("AlreadyExists", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { getWidget Monitor, "widget1" } `), - "assets/index.json": []byte(`{"zorder":[{"name":"widget1"},{"name":"widget2"}]}`), - } + "assets/index.json": []byte(`{"zorder":[{"name":"widget1"},{"name":"widget2"}]}`), }), nil) result, err := s.compile() require.NoError(t, err) diff --git a/tools/spxls/internal/server/semantic_token_test.go b/tools/spxls/internal/server/semantic_token_test.go index 6b159b46a..aaab1e145 100644 --- a/tools/spxls/internal/server/semantic_token_test.go +++ b/tools/spxls/internal/server/semantic_token_test.go @@ -3,29 +3,26 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestServerTextDocumentSemanticTokensFull(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` var ( MySprite Sprite ) MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) mainSpxTokens, err := s.textDocumentSemanticTokensFull(&SemanticTokensParams{ diff --git a/tools/spxls/internal/server/server.go b/tools/spxls/internal/server/server.go index e21b8f7ec..46e85884c 100644 --- a/tools/spxls/internal/server/server.go +++ b/tools/spxls/internal/server/server.go @@ -6,6 +6,7 @@ import ( "go/types" "io/fs" "strings" + "sync" "github.com/goplus/builder/tools/spxls/internal" "github.com/goplus/builder/tools/spxls/internal/jsonrpc2" @@ -30,6 +31,8 @@ type Server struct { spxResourceRootDir string replier MessageReplier importer types.Importer + compileCacheMu sync.Mutex + lastCompileCache *compileCache } // New creates a new Server instance. @@ -322,7 +325,7 @@ func (s *Server) spxFiles() ([]string, error) { if err != nil { return nil, err } - var files []string + files := make([]string, 0, len(entries)) for _, entry := range entries { if entry.IsDir() { continue diff --git a/tools/spxls/internal/server/server_test.go b/tools/spxls/internal/server/server_test.go new file mode 100644 index 000000000..c1bd6c768 --- /dev/null +++ b/tools/spxls/internal/server/server_test.go @@ -0,0 +1,13 @@ +package server + +import "github.com/goplus/builder/tools/spxls/internal/vfs" + +func newMapFSWithoutModTime(files map[string][]byte) *vfs.MapFS { + return vfs.NewMapFS(func() map[string]vfs.MapFile { + fileMap := make(map[string]vfs.MapFile) + for k, v := range files { + fileMap[k] = vfs.MapFile{Content: v} + } + return fileMap + }) +} diff --git a/tools/spxls/internal/server/signature_test.go b/tools/spxls/internal/server/signature_test.go index c93c783c3..727df4b16 100644 --- a/tools/spxls/internal/server/signature_test.go +++ b/tools/spxls/internal/server/signature_test.go @@ -3,16 +3,14 @@ package server import ( "testing" - "github.com/goplus/builder/tools/spxls/internal/vfs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestTextDocumentSignatureHelp(t *testing.T) { t.Run("Normal", func(t *testing.T) { - s := New(vfs.NewMapFS(func() map[string][]byte { - return map[string][]byte{ - "main.spx": []byte(` + s := New(newMapFSWithoutModTime(map[string][]byte{ + "main.spx": []byte(` import "fmt" var ( MySprite Sprite @@ -21,13 +19,12 @@ fmt.Println MySprite.turn Left run "assets", {Title: "My Game"} `), - "MySprite.spx": []byte(` + "MySprite.spx": []byte(` onStart => { MySprite.turn Right } `), - "assets/sprites/MySprite/index.json": []byte(`{}`), - } + "assets/sprites/MySprite/index.json": []byte(`{}`), }), nil) help, err := s.textDocumentSignatureHelp(&SignatureHelpParams{ diff --git a/tools/spxls/internal/vfs/mapfs.go b/tools/spxls/internal/vfs/mapfs.go index 469e05474..77a87f8cf 100644 --- a/tools/spxls/internal/vfs/mapfs.go +++ b/tools/spxls/internal/vfs/mapfs.go @@ -9,15 +9,20 @@ import ( "time" ) +// MapFile represents a file's content and metadata in the map file system. +type MapFile struct { + Content []byte + ModTime time.Time +} + // GetFileMapFunc is the type for function that returns a map of files. -type GetFileMapFunc func() map[string][]byte +type GetFileMapFunc func() map[string]MapFile // MapFS implements [fs.ReadDirFS] using a map of files. type MapFS struct { getFileMap GetFileMapFunc fileMode fs.FileMode dirMode fs.FileMode - modTime time.Time } // NewMapFS creates a new map file system. @@ -26,7 +31,6 @@ func NewMapFS(getFileMap GetFileMapFunc) *MapFS { getFileMap: getFileMap, fileMode: 0444, dirMode: 0444 | fs.ModeDir, - modTime: time.Now(), } } @@ -39,15 +43,15 @@ func (mfs *MapFS) Open(name string) (fs.File, error) { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} } - content, ok := fileMap[name] + mf, ok := fileMap[name] if !ok { return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} } return &file{ name: name, - content: content, + content: mf.Content, mode: mfs.fileMode, - modTime: mfs.modTime, + modTime: mf.ModTime, }, nil } @@ -97,19 +101,27 @@ func (mfs *MapFS) ReadDir(name string) ([]fs.DirEntry, error) { entries := make([]fs.DirEntry, 0, len(dirs)+len(files)) for d := range dirs { + var latestModTime time.Time + dirPrefix := prefix + d + "/" + for p, mf := range fileMap { + if strings.HasPrefix(p, dirPrefix) && mf.ModTime.After(latestModTime) { + latestModTime = mf.ModTime + } + } entries = append(entries, &dirEntry{ name: d, mode: mfs.dirMode, - modTime: mfs.modTime, + modTime: latestModTime, isDir: true, }) } for f := range files { + mf := fileMap[prefix+f] entries = append(entries, &dirEntry{ name: f, - size: int64(len(fileMap[prefix+f])), + size: int64(len(mf.Content)), mode: mfs.fileMode, - modTime: mfs.modTime, + modTime: mf.ModTime, isDir: false, }) } @@ -127,21 +139,27 @@ func (mfs *MapFS) Stat(name string) (fs.FileInfo, error) { fileMap := mfs.getFileMap() if name == "." { + var latestModTime time.Time + for _, mf := range fileMap { + if mf.ModTime.After(latestModTime) { + latestModTime = mf.ModTime + } + } return &fileInfo{ name: ".", mode: mfs.dirMode, - modTime: mfs.modTime, + modTime: latestModTime, isDir: true, }, nil } - content, ok := fileMap[name] + mf, ok := fileMap[name] if ok { return &fileInfo{ name: path.Base(name), - size: int64(len(content)), + size: int64(len(mf.Content)), mode: mfs.fileMode, - modTime: mfs.modTime, + modTime: mf.ModTime, isDir: false, }, nil } @@ -149,10 +167,13 @@ func (mfs *MapFS) Stat(name string) (fs.FileInfo, error) { // Check if it's a directory by looking for files with this prefix. prefix := name + "/" hasPrefix := false - for p := range fileMap { + var latestModTime time.Time + for p, mf := range fileMap { if strings.HasPrefix(p, prefix) { hasPrefix = true - break + if mf.ModTime.After(latestModTime) { + latestModTime = mf.ModTime + } } } @@ -160,7 +181,7 @@ func (mfs *MapFS) Stat(name string) (fs.FileInfo, error) { return &fileInfo{ name: path.Base(name), mode: mfs.dirMode, - modTime: mfs.modTime, + modTime: latestModTime, isDir: true, }, nil } diff --git a/tools/spxls/internal/vfs/mapfs_test.go b/tools/spxls/internal/vfs/mapfs_test.go index 6f319308a..e8f7b17d3 100644 --- a/tools/spxls/internal/vfs/mapfs_test.go +++ b/tools/spxls/internal/vfs/mapfs_test.go @@ -8,14 +8,14 @@ import ( "testing" ) -func newTestMapFS() (fs.FS, map[string][]byte) { - files := map[string][]byte{ - "foo.txt": []byte("foo"), - "dir/bar.txt": []byte("bar"), - "dir/subdir/another.txt": []byte("another"), - "other/file.txt": []byte("other"), +func newTestMapFS() (fs.FS, map[string]MapFile) { + files := map[string]MapFile{ + "foo.txt": {Content: []byte("foo")}, + "dir/bar.txt": {Content: []byte("bar")}, + "dir/subdir/another.txt": {Content: []byte("another")}, + "other/file.txt": {Content: []byte("other")}, } - fsys := NewMapFS(func() map[string][]byte { + fsys := NewMapFS(func() map[string]MapFile { return files }) return fsys, files @@ -36,7 +36,7 @@ func TestMapFSOpen(t *testing.T) { t.Fatal(err) } - want := files["foo.txt"] + want := files["foo.txt"].Content if !bytes.Equal(got, want) { t.Errorf("content mismatch: got %q, want %q", got, want) } @@ -160,8 +160,8 @@ func TestMapFSFile(t *testing.T) { t.Fatal(err) } - if size := info.Size(); size != int64(len(want)) { - t.Errorf("size mismatch: got %d, want %d", size, len(want)) + if size := info.Size(); size != int64(len(want.Content)) { + t.Errorf("size mismatch: got %d, want %d", size, len(want.Content)) } got, err := io.ReadAll(f) @@ -169,8 +169,8 @@ func TestMapFSFile(t *testing.T) { t.Fatal(err) } - if !bytes.Equal(got, want) { - t.Errorf("content mismatch: got %q, want %q", got, want) + if !bytes.Equal(got, want.Content) { + t.Errorf("content mismatch: got %q, want %q", got, want.Content) } }) } diff --git a/tools/spxls/main.go b/tools/spxls/main.go index ac467a4cd..30682f813 100644 --- a/tools/spxls/main.go +++ b/tools/spxls/main.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "syscall/js" + "time" "github.com/goplus/builder/tools/spxls/internal/jsonrpc2" "github.com/goplus/builder/tools/spxls/internal/server" @@ -33,7 +34,7 @@ func NewSpxls(this js.Value, args []js.Value) any { s := &Spxls{ messageReplier: args[1], } - s.server = server.New(vfs.NewMapFS(func() map[string][]byte { + s.server = server.New(vfs.NewMapFS(func() map[string]vfs.MapFile { files := filesProvider.Invoke() return ConvertJSFilesToMap(files) }), s) @@ -104,17 +105,20 @@ func JSUint8ArrayToBytes(uint8Array js.Value) []byte { } // ConvertJSFilesToMap converts a JavaScript object of files to a map. -func ConvertJSFilesToMap(files js.Value) map[string][]byte { +func ConvertJSFilesToMap(files js.Value) map[string]vfs.MapFile { if files.Type() != js.TypeObject { return nil } - result := make(map[string][]byte) keys := js.Global().Get("Object").Call("keys", files) + result := make(map[string]vfs.MapFile, keys.Length()) for i := range keys.Length() { key := keys.Index(i).String() value := files.Get(key) if value.InstanceOf(js.Global().Get("Object")) { - result[key] = JSUint8ArrayToBytes(value.Get("content")) + result[key] = vfs.MapFile{ + Content: JSUint8ArrayToBytes(value.Get("content")), + ModTime: time.UnixMilli(int64(value.Get("modTime").Int())), + } } } return result