2 files changed,
114 insertions(+),
3 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-08-16 16:32:29 +0300
Authored at:
2026-08-16 13:47:32 +0300
Change ID:
swmnksttpstovtupxzptmlzzukrvvuro
Parent:
9d057cb
jump to
| M | internal/lsp/server.go |
| M | internal/lsp/server_test.go |
M
internal/lsp/server.go
··· 26 26 loader *journal.Loader 27 27 printer *printer.Config 28 28 29 - mu sync.Mutex 30 - openDocs map[uri.URI]docState 31 - diagCancel context.CancelFunc 29 + mu sync.Mutex 30 + openDocs map[uri.URI]docState 31 + diagCancel context.CancelFunc 32 + supportsDynamicFileWatchers bool 32 33 33 34 cfgMu sync.RWMutex 34 35 config Config ··· 72 73 } 73 74 74 75 func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) { 76 + if w := params.Capabilities.Workspace; w != nil { 77 + if wf := w.DidChangeWatchedFiles; wf != nil { 78 + s.supportsDynamicFileWatchers = wf.DynamicRegistration != nil && *wf.DynamicRegistration 79 + } 80 + } 81 + 75 82 s.applySettings(params.InitializationOptions) 76 83 return &protocol.InitializeResult{ 77 84 ServerInfo: protocol.ServerInfo{ ··· 105 112 } 106 113 107 114 func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { 115 + if s.supportsDynamicFileWatchers { 116 + go s.registerFileWatchers(context.Background()) 117 + } 118 + s.scheduleDiagnostics(ctx) 119 + return nil 120 +} 121 + 122 +func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { 123 + for _, change := range params.Changes { 124 + u := change.URI 125 + path := u.Path() 126 + if path == "" { 127 + continue 128 + } 129 + if _, isOpen := s.getDocState(u); isOpen { 130 + continue // editor buffer is authoritative for open docs 131 + } 132 + s.loader.InvalidateFile(path) 133 + s.markDependentsDirty(u) 134 + } 108 135 s.scheduleDiagnostics(ctx) 109 136 return nil 110 137 } ··· 120 147 121 148 func (s *server) Exit(ctx context.Context) error { 122 149 return nil 150 +} 151 + 152 +func (s *server) registerFileWatchers(ctx context.Context) { 153 + if s.client == nil { 154 + return 155 + } 156 + watchers := make([]protocol.FileSystemWatcher, 0, len(journal.SupportedExtensions)) 157 + for _, ext := range journal.SupportedExtensions { 158 + watchers = append(watchers, protocol.FileSystemWatcher{GlobPattern: protocol.Pattern("**/*" + ext)}) 159 + } 160 + options, err := protocol.Marshal(protocol.DidChangeWatchedFilesRegistrationOptions{Watchers: watchers}) 161 + if err != nil { 162 + return 163 + } 164 + if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ 165 + Registrations: []protocol.Registration{{ 166 + ID: "clerk.watchedFiles", 167 + Method: protocol.MethodWorkspaceDidChangeWatchedFiles, 168 + RegisterOptions: protocol.LSPAny(options), 169 + }}, 170 + }); err != nil { 171 + s.log.Warn("registering file watchers failed", "err", err) 172 + } 123 173 } 124 174 125 175 func (s *server) applySettings(v protocol.LSPAny) {
M
internal/lsp/server_test.go
··· 123 123 } 124 124 } 125 125 126 +func TestServer_DidChangeWatchedFiles_SkipsOpenDocuments(t *testing.T) { 127 + dir := t.TempDir() 128 + base := filepath.Join(dir, "base.journal") 129 + testutil.WriteFile(t, base, []byte("2024-01-01 t\n expenses:food $10\n assets:cash\n")) 130 + 131 + srv := NewServer("test") 132 + uBase := uri.File(base) 133 + srv.server.openDoc(uBase, "2024-01-01 t\n expenses:food $10\n assets:cash\n", 1, "journal") 134 + 135 + a1 := srv.server.analysisFor(uBase) 136 + 137 + testutil.WriteFile(t, base, []byte("2024-01-01 t\n expenses:food $10\n assets:bank\n")) 138 + if err := srv.server.DidChangeWatchedFiles(context.Background(), &protocol.DidChangeWatchedFilesParams{ 139 + Changes: []protocol.FileEvent{{URI: uBase, Type: protocol.FileChangeTypeChanged}}, 140 + }); err != nil { 141 + t.Fatalf("didChangeWatchedFiles: %v", err) 142 + } 143 + 144 + if a2 := srv.server.analysisFor(uBase); a2 != a1 { 145 + t.Error("open document rebuilt from disk; buffer is authoritative") 146 + } 147 +} 148 + 149 +func TestServer_DidChangeWatchedFiles_DiskChangeDirtiesDependents(t *testing.T) { 150 + dir := t.TempDir() 151 + base := filepath.Join(dir, "base.journal") 152 + main := filepath.Join(dir, "main.journal") 153 + testutil.WriteFile(t, base, []byte("2024-01-01 t\n expenses:food $10\n assets:cash\n")) 154 + testutil.WriteFile(t, main, []byte("include base.journal\n")) 155 + 156 + srv := NewServer("test") 157 + srv.server.client = &captureClient{} 158 + uMain := uri.File(main) 159 + if err := srv.server.DidOpen(context.Background(), &protocol.DidOpenTextDocumentParams{ 160 + TextDocument: protocol.TextDocumentItem{URI: uMain, LanguageID: "journal", Version: 1, Text: "include base.journal\n"}, 161 + }); err != nil { 162 + t.Fatalf("didOpen: %v", err) 163 + } 164 + 165 + a1 := srv.server.analysisFor(uMain) 166 + if !slices.Contains(a1.AccountNames, "assets:cash") { 167 + t.Fatalf("initial analysis missing included account: %v", a1.AccountNames) 168 + } 169 + 170 + // base changes on disk, outside the editor 171 + testutil.WriteFile(t, base, []byte("2024-01-01 t\n expenses:food $10\n assets:bank\n")) 172 + if err := srv.server.DidChangeWatchedFiles(context.Background(), &protocol.DidChangeWatchedFilesParams{ 173 + Changes: []protocol.FileEvent{{URI: uri.File(base), Type: protocol.FileChangeTypeChanged}}, 174 + }); err != nil { 175 + t.Fatalf("didChangeWatchedFiles: %v", err) 176 + } 177 + 178 + a2 := srv.server.analysisFor(uMain) 179 + if a2 == a1 { 180 + t.Error("disk change did not rebuild the dependent analysis") 181 + } 182 + if !slices.Contains(a2.AccountNames, "assets:bank") { 183 + t.Errorf("stale included content after disk change: %v", a2.AccountNames) 184 + } 185 +} 186 + 126 187 type captureClient struct { 127 188 protocol.Client 128 189 mu sync.Mutex