Skip to content

Commit a2ad8dd

Browse files
committed
feat: improve indexer performance
1 parent 0d60bc1 commit a2ad8dd

File tree

7 files changed

+183
-74
lines changed

7 files changed

+183
-74
lines changed

.claude/settings.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"hooks": {
3+
"SessionStart": [
4+
{
5+
"matcher": "startup",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "if [ -f \"$CLAUDE_PROJECT_DIR/AGENTS.md\" ]; then cat \"$CLAUDE_PROJECT_DIR/AGENTS.md\"; fi"
10+
},
11+
{
12+
"type": "command",
13+
"command": "if [ -f \"$CLAUDE_PROJECT_DIR/docs/docs/contrib-code.md\" ]; then cat \"$CLAUDE_PROJECT_DIR/docs/docs/contrib-code.md\"; fi"
14+
}
15+
]
16+
}
17+
]
18+
}
19+
}

.windsurfrules

Lines changed: 0 additions & 35 deletions
This file was deleted.

AGENTS.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

AGENTS.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Project Overview
2+
3+
Shopware LSP is a Language Server Protocol implementation for Shopware and Symfony development. It provides IDE features (completion, go-to-definition, hover, diagnostics) for PHP, Twig, XML, and YAML files.
4+
5+
**Tech Stack:** Go backend with tree-sitter parsing, BBolt embedded database for indexes, TypeScript VSCode extension.
6+
7+
## Build & Test Commands
8+
9+
```bash
10+
# Build
11+
go build # Build LSP server binary
12+
go build ./... # Build all packages
13+
14+
# Test
15+
go test ./... # Run all tests
16+
go test -race ./internal/... # Race detection (used in CI)
17+
go test ./internal/php/... -v # Test specific package
18+
go test -run TestFeatureIndexer # Run specific test
19+
20+
# Lint
21+
golangci-lint run # Lint check (run before committing)
22+
23+
# VSCode extension
24+
cd vscode-extension
25+
npm install && npm run compile # Build extension
26+
npm run check-types # Type check only
27+
```
28+
29+
## Architecture
30+
31+
### Entry Point
32+
`main.go` initializes the LSP server, registers all indexers and providers, then starts on stdin/stdout (JSON-RPC).
33+
34+
### Key Packages (`internal/`)
35+
36+
| Package | Purpose |
37+
|---------|---------|
38+
| `lsp/` | LSP protocol, server.go is the main handler |
39+
| `lsp/completion/` | 7 completion providers (services, routes, twig, snippets, features, system config, theme) |
40+
| `lsp/definition/` | 7 go-to-definition providers (same domains) |
41+
| `indexer/` | FileScanner for file watching, DataIndexer for BBolt persistence |
42+
| `symfony/` | Service container and route indexing from XML/YAML/PHP |
43+
| `php/` | PHP class/method indexing, type inference, alias resolution |
44+
| `twig/` | Template indexing, block tracking, extends/include parsing |
45+
| `snippet/` | Translation key indexing from JSON files |
46+
| `feature/` | Feature flag indexing from YAML |
47+
| `theme/` | Theme config and icon indexing |
48+
| `extension/` | Shopware extension metadata from composer.json |
49+
50+
### Provider Pattern
51+
All LSP features use a provider interface pattern. Multiple providers can handle the same feature type, routed by document language/context.
52+
53+
### Indexing Flow
54+
1. `FileScanner` detects file changes (fsnotify)
55+
2. Files parsed with tree-sitter based on type
56+
3. Each registered indexer processes AST nodes
57+
4. Data persisted to BBolt databases in cache directory
58+
59+
## Testing Patterns
60+
61+
Tests use `testify/assert` and `testify/require`. Common pattern:
62+
```go
63+
func TestSomething(t *testing.T) {
64+
tempDir := t.TempDir()
65+
indexer, err := NewIndexer(tempDir)
66+
require.NoError(t, err)
67+
defer indexer.Close()
68+
// ... test logic
69+
}
70+
```
71+
72+
## Commit Guidelines
73+
74+
Use Conventional Commits: `type(scope): summary`
75+
- `fix:` bug fixes
76+
- `feat:` new features
77+
- `build:` build system changes
78+
- `docs:` documentation
79+
80+
## Debug Tool
81+
82+
```bash
83+
go run cmd/debug_ast/main.go path/to/file.php # Visualize PHP AST
84+
```

CLAUDE.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ replace github.com/tree-sitter-grammars/tree-sitter-xml => github.com/justinMBul
77
replace github.com/tree-sitter-grammars/tree-sitter-scss => github.com/shyim/tree-sitter-scss v0.0.0-20250502122635-ca898ab73795
88

99
require (
10-
github.com/cespare/xxhash/v2 v2.3.0
1110
github.com/fsnotify/fsnotify v1.9.0
1211
github.com/sourcegraph/jsonrpc2 v0.2.1
1312
github.com/stretchr/testify v1.10.0

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
2-
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
31
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
42
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
53
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=

internal/indexer/filescanner.go

Lines changed: 80 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -454,27 +454,41 @@ func (fs *FileScanner) RemoveFiles(ctx context.Context, paths []string) error {
454454
return nil
455455
}
456456

457-
// removeFileFromIndexers removes a single file from all indexers without affecting the hash database
458-
func (fs *FileScanner) removeFileFromIndexers(path string) error {
457+
func (fs *FileScanner) removeFilesFromIndexers(paths []string) error {
459458
for _, indexer := range fs.indexer {
460-
if err := indexer.RemovedFiles([]string{path}); err != nil {
459+
if err := indexer.RemovedFiles(paths); err != nil {
461460
return err
462461
}
463462
}
464463
return nil
465464
}
466465

467-
// updateFileState updates the stored file size and mtime for a file
468-
func (fs *FileScanner) updateFileState(path string, info os.FileInfo) error {
466+
func (fs *FileScanner) updateFileStates(files []fileState) error {
469467
return fs.db.Update(func(tx *bbolt.Tx) error {
470468
hashBucket := tx.Bucket([]byte("file_hashes"))
471-
stateBytes := make([]byte, 16)
472-
binary.LittleEndian.PutUint64(stateBytes[:8], uint64(info.Size()))
473-
binary.LittleEndian.PutUint64(stateBytes[8:], uint64(info.ModTime().UnixNano()))
474-
return hashBucket.Put([]byte(path), stateBytes)
469+
for _, file := range files {
470+
stateBytes := make([]byte, 16)
471+
binary.LittleEndian.PutUint64(stateBytes[:8], uint64(file.info.Size()))
472+
binary.LittleEndian.PutUint64(stateBytes[8:], uint64(file.info.ModTime().UnixNano()))
473+
if err := hashBucket.Put([]byte(file.path), stateBytes); err != nil {
474+
return err
475+
}
476+
}
477+
return nil
475478
})
476479
}
477480

481+
type fileState struct {
482+
path string
483+
info os.FileInfo
484+
}
485+
486+
type fileWork struct {
487+
path string
488+
content []byte
489+
info os.FileInfo
490+
}
491+
478492
// IndexFiles processes multiple files in parallel
479493
func (fs *FileScanner) IndexFiles(ctx context.Context, files []string) error {
480494
if len(files) == 0 {
@@ -532,49 +546,81 @@ func (fs *FileScanner) IndexFiles(ctx context.Context, files []string) error {
532546
defer wg.Done()
533547

534548
parsers := CreateTreesitterParsers()
549+
const batchSize = 50
550+
batch := make([]fileWork, 0, batchSize)
535551

536-
for path := range fileChan {
537-
// Check if file needs indexing
538-
needsIndexing, content, info, err := fs.fileNeedsIndexing(path)
539-
if err != nil {
540-
// We'll just skip file errors to reduce noise
541-
continue
552+
processBatch := func(items []fileWork) {
553+
if len(items) == 0 {
554+
return
542555
}
543556

544-
// If file hasn't changed, skip it
545-
if !needsIndexing {
546-
continue
557+
paths := make([]string, 0, len(items))
558+
for _, item := range items {
559+
paths = append(paths, item.path)
547560
}
548561

549-
// Remove the file from all indexers since we're reindexing it
550-
if err := fs.removeFileFromIndexers(path); err != nil {
562+
if err := fs.removeFilesFromIndexers(paths); err != nil {
551563
errChan <- err
552-
continue
564+
return
553565
}
554566

555-
ext := strings.ToLower(filepath.Ext(path))
556-
557-
parser := parsers[ext]
558-
if parser == nil {
559-
panic(fmt.Sprintf("no parser found for file type: %s", ext))
560-
}
567+
for _, item := range items {
568+
ext := strings.ToLower(filepath.Ext(item.path))
569+
parser := parsers[ext]
570+
if parser == nil {
571+
panic(fmt.Sprintf("no parser found for file type: %s", ext))
572+
}
561573

562-
tree := parser.Parse(content, nil)
574+
tree := parser.Parse(item.content, nil)
563575

564-
for _, indexer := range fs.indexer {
565-
if err := indexer.Index(path, tree.RootNode(), content); err != nil {
566-
errChan <- err
576+
for _, indexer := range fs.indexer {
577+
if err := indexer.Index(item.path, tree.RootNode(), item.content); err != nil {
578+
errChan <- err
579+
}
567580
}
581+
582+
tree.Close()
568583
}
569584

570-
tree.Close()
585+
fileStates := make([]fileState, 0, len(items))
586+
for _, item := range items {
587+
fileStates = append(fileStates, fileState{
588+
path: item.path,
589+
info: item.info,
590+
})
591+
}
571592

572-
// Update the file hash
573-
if err := fs.updateFileState(path, info); err != nil {
593+
if err := fs.updateFileStates(fileStates); err != nil {
574594
errChan <- err
575595
}
576596
}
577597

598+
for path := range fileChan {
599+
// Check if file needs indexing
600+
needsIndexing, content, info, err := fs.fileNeedsIndexing(path)
601+
if err != nil {
602+
// We'll just skip file errors to reduce noise
603+
continue
604+
}
605+
606+
// If file hasn't changed, skip it
607+
if !needsIndexing {
608+
continue
609+
}
610+
611+
batch = append(batch, fileWork{
612+
path: path,
613+
content: content,
614+
info: info,
615+
})
616+
if len(batch) >= batchSize {
617+
processBatch(batch)
618+
batch = batch[:0]
619+
}
620+
}
621+
622+
processBatch(batch)
623+
578624
CloseTreesitterParsers(parsers)
579625
}()
580626
}

0 commit comments

Comments
 (0)