all repos

clerk @ 2963f4c

missing tooling for ledger/hledger
2 files changed, 114 insertions(+), 3 deletions(-)
lsp: implement workspace/didChangeWatchedFiles
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
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