all repos

clerk @ 3d92c0e

missing tooling for ledger/hledger
11 files changed, 464 insertions(+), 7 deletions(-)
lsp: init server, report diagnostics
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-12 13:17:30 +0300
Authored at: 2026-07-11 12:55:38 +0300
Change ID: lvtwmkztnmkmrpxxqstvyqmonpvktvvn
Parent: cea9a8c
M go.mod
···
        4
        4
         

      
        5
        5
         tool golang.org/x/tools/cmd/stringer

      
        6
        6
         

      
        7
        
        -require github.com/urfave/cli/v3 v3.10.0

      
        
        7
        +require (

      
        
        8
        +	github.com/urfave/cli/v3 v3.10.0

      
        
        9
        +	go.lsp.dev/jsonrpc2 v1.0.1

      
        
        10
        +	go.lsp.dev/protocol v1.0.1

      
        
        11
        +	go.lsp.dev/uri v1.0.1

      
        
        12
        +)

      
        8
        13
         

      
        9
        14
         require (

      
        
        15
        +	github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect

      
        10
        16
         	golang.org/x/mod v0.36.0 // indirect

      
        11
        17
         	golang.org/x/sync v0.20.0 // indirect

      
        12
        18
         	golang.org/x/tools v0.45.0 // indirect

      
M go.sum
···
        1
        1
         github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=

      
        2
        2
         github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

      
        3
        
        -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=

      
        4
        
        -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=

      
        
        3
        +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=

      
        
        4
        +github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=

      
        
        5
        +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=

      
        
        6
        +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=

      
        5
        7
         github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=

      
        6
        8
         github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

      
        7
        9
         github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=

      
        8
        10
         github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=

      
        9
        11
         github.com/urfave/cli/v3 v3.10.0 h1:0aU8yOObVDMkM13Cj4G+zb4P0PdeJMec65f81Ak1ioM=

      
        10
        12
         github.com/urfave/cli/v3 v3.10.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=

      
        
        13
        +go.lsp.dev/jsonrpc2 v1.0.1 h1:JZAchpm6RcOuwEOnZ7kF3kA1EpEjdJZ/6rMSfOF3+F4=

      
        
        14
        +go.lsp.dev/jsonrpc2 v1.0.1/go.mod h1:WN+TesqrPumBmUdsuWyn9DHq3zl4i85L/qG0NLdj8qg=

      
        
        15
        +go.lsp.dev/protocol v1.0.1 h1:kLWUIk1v/deXxo1D0VO4avjZnfeuZ82mouJgJd/NbdU=

      
        
        16
        +go.lsp.dev/protocol v1.0.1/go.mod h1:e//i41aEqc9ml32xiNuneUwbjOjuvTA4+uv+AkssJgk=

      
        
        17
        +go.lsp.dev/uri v1.0.1 h1:ywxJN1UXoQ+v0I9lkoQNMmsm7lub5G9ZG7KXT8YTx30=

      
        
        18
        +go.lsp.dev/uri v1.0.1/go.mod h1:06a0ghafs4PuqkLsJGUmlgPHw6+kH79edRXjOT/rA8s=

      
        11
        19
         golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=

      
        12
        20
         golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=

      
        13
        21
         golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=

      
M internal/cli/cli.go
···
        58
        58
         				},

      
        59
        59
         			},

      
        60
        60
         			{

      
        
        61
        +				Name:   "lsp",

      
        
        62
        +				Usage:  "lsp server",

      
        
        63
        +				Action: c.lspAction,

      
        
        64
        +			},

      
        
        65
        +			{

      
        61
        66
         				Name:   "tags",

      
        62
        67
         				Usage:  "generate a tags file for journal entries",

      
        63
        68
         				Action: c.tagsAction,

      
M internal/cli/cmd_lint.go
···
        43
        43
         		}

      
        44
        44
         	}

      
        45
        45
         

      
        46
        
        -	sema := semantic.Build(loader.Ordered())

      
        47
        
        -	reporter.Collect(lint.Run(sema))

      
        
        46
        +	for _, root := range loader.Roots() {

      
        
        47
        +		reporter.Collect(lint.Run(semantic.Build(journal.CollectFiles(root))))

      
        
        48
        +	}

      
        48
        49
         

      
        49
        50
         	if err := reporter.Flush(format); err != nil {

      
        50
        51
         		return fmt.Errorf("flushing report: %w", err)

      ···
        62
        63
         		return err

      
        63
        64
         	}

      
        64
        65
         

      
        65
        
        -	sema := semantic.Build(loader.Ordered())

      
        66
        
        -	reporter.Collect(lint.Run(sema))

      
        
        66
        +	for _, root := range loader.Roots() {

      
        
        67
        +		reporter.Collect(lint.Run(semantic.Build(journal.CollectFiles(root))))

      
        
        68
        +	}

      
        67
        69
         

      
        68
        70
         	if err := reporter.Flush(format); err != nil {

      
        69
        71
         		return fmt.Errorf("flushing report: %w", err)

      
A internal/cli/cmd_lsp.go
···
        
        1
        +package cli

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"os"

      
        
        6
        +

      
        
        7
        +	"github.com/urfave/cli/v3"

      
        
        8
        +	"olexsmir.xyz/clerk/internal/lsp"

      
        
        9
        +)

      
        
        10
        +

      
        
        11
        +func (c *Cli) lspAction(ctx context.Context, cmd *cli.Command) error {

      
        
        12
        +	server := lsp.NewServer(c.version)

      
        
        13
        +	return server.Run(ctx, os.Stdin, os.Stdout)

      
        
        14
        +}

      
A internal/lsp/diagnostics.go
···
        
        1
        +package lsp

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"time"

      
        
        7
        +

      
        
        8
        +	"go.lsp.dev/jsonrpc2"

      
        
        9
        +	"go.lsp.dev/protocol"

      
        
        10
        +	"go.lsp.dev/uri"

      
        
        11
        +	"olexsmir.xyz/clerk/internal/linter"

      
        
        12
        +	"olexsmir.xyz/clerk/journal"

      
        
        13
        +	"olexsmir.xyz/clerk/journal/semantic"

      
        
        14
        +	"olexsmir.xyz/clerk/journal/token"

      
        
        15
        +)

      
        
        16
        +

      
        
        17
        +func (s *server) rebuildWorkspace(ctx context.Context) {

      
        
        18
        +	s.log.Debug("rebuilding workspace")

      
        
        19
        +

      
        
        20
        +	s.mu.Lock()

      
        
        21
        +	defer s.mu.Unlock()

      
        
        22
        +

      
        
        23
        +	if len(s.openDocs) == 0 {

      
        
        24
        +		return

      
        
        25
        +	}

      
        
        26
        +

      
        
        27
        +	for uri, state := range s.openDocs {

      
        
        28
        +		path := uri.Path()

      
        
        29
        +		if _, err := s.loader.Reload(path, []byte(state.text)); err != nil {

      
        
        30
        +			s.log.Warn("filed to reload open document", "uri", uri, "err", err)

      
        
        31
        +		}

      
        
        32
        +	}

      
        
        33
        +

      
        
        34
        +	roots := s.loader.Roots()

      
        
        35
        +	if len(roots) == 0 {

      
        
        36
        +		s.log.Debug("no files in workspace")

      
        
        37
        +		return

      
        
        38
        +	}

      
        
        39
        +

      
        
        40
        +	var allFiles []*journal.ParsedFile

      
        
        41
        +	var finds []linter.Find

      
        
        42
        +	for _, root := range roots {

      
        
        43
        +		files := journal.CollectFiles(root)

      
        
        44
        +		allFiles = append(allFiles, files...)

      
        
        45
        +		c := semantic.Build(files)

      
        
        46
        +		finds = append(finds, s.linter.Run(c)...)

      
        
        47
        +

      
        
        48
        +	}

      
        
        49
        +

      
        
        50
        +	finds = dedupFinds(finds)

      
        
        51
        +	s.semanticCtx = semantic.Build(allFiles)

      
        
        52
        +

      
        
        53
        +	diagsByFile := s.groupFindsByFile(finds)

      
        
        54
        +	activeFiles := s.activeFileSet(s.openDocs, s.loader.Ordered())

      
        
        55
        +	for fpath := range activeFiles {

      
        
        56
        +		if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{

      
        
        57
        +			URI:         uri.File(fpath),

      
        
        58
        +			Diagnostics: diagsByFile[fpath],

      
        
        59
        +		}); err != nil {

      
        
        60
        +			s.log.Warn("publish diagnostics failed", "uri", uri.File(fpath), "err", err)

      
        
        61
        +		}

      
        
        62
        +	}

      
        
        63
        +

      
        
        64
        +	s.log.Debug("workspace rebuild complete",

      
        
        65
        +		"files", len(s.loader.Ordered()), "findings", len(finds))

      
        
        66
        +}

      
        
        67
        +

      
        
        68
        +func (s *server) scheduleWorkspaceRebuild(ctx context.Context) {

      
        
        69
        +	s.mu.Lock()

      
        
        70
        +	defer s.mu.Unlock()

      
        
        71
        +	if s.debouncer != nil {

      
        
        72
        +		s.debouncer.Stop()

      
        
        73
        +	}

      
        
        74
        +	s.debouncer = time.AfterFunc(200*time.Millisecond, func() {

      
        
        75
        +		s.rebuildWorkspace(jsonrpc2.DetachContext(ctx))

      
        
        76
        +	})

      
        
        77
        +}

      
        
        78
        +

      
        
        79
        +func (s *server) cancelWorkspaceRebuild() {

      
        
        80
        +	s.mu.Lock()

      
        
        81
        +	defer s.mu.Unlock()

      
        
        82
        +	if s.debouncer != nil {

      
        
        83
        +		s.debouncer.Stop()

      
        
        84
        +		s.debouncer = nil

      
        
        85
        +	}

      
        
        86
        +}

      
        
        87
        +

      
        
        88
        +func (s *server) activeFileSet(docs map[uri.URI]docState, ordered []*journal.ParsedFile) map[string]bool {

      
        
        89
        +	active := make(map[string]bool)

      
        
        90
        +	for duri := range docs {

      
        
        91
        +		active[duri.Path()] = true

      
        
        92
        +	}

      
        
        93
        +	visited := make(map[string]bool)

      
        
        94
        +	var walk func(*journal.ParsedFile)

      
        
        95
        +	walk = func(pf *journal.ParsedFile) {

      
        
        96
        +		if visited[pf.Path] {

      
        
        97
        +			return

      
        
        98
        +		}

      
        
        99
        +		active[pf.Path] = true

      
        
        100
        +		visited[pf.Path] = true

      
        
        101
        +		for _, inc := range pf.Includes {

      
        
        102
        +			walk(inc)

      
        
        103
        +		}

      
        
        104
        +	}

      
        
        105
        +	byPath := make(map[string]*journal.ParsedFile, len(ordered))

      
        
        106
        +	for _, pf := range ordered {

      
        
        107
        +		byPath[pf.Path] = pf

      
        
        108
        +	}

      
        
        109
        +	for duri := range docs {

      
        
        110
        +		if pf, ok := byPath[duri.Path()]; ok {

      
        
        111
        +			walk(pf)

      
        
        112
        +		}

      
        
        113
        +	}

      
        
        114
        +	return active

      
        
        115
        +}

      
        
        116
        +

      
        
        117
        +func (s *server) groupFindsByFile(finds []linter.Find) map[string][]protocol.Diagnostic {

      
        
        118
        +	diags := make(map[string][]protocol.Diagnostic)

      
        
        119
        +	for _, find := range finds {

      
        
        120
        +		file := find.Span.Start.File

      
        
        121
        +		if file == "" {

      
        
        122
        +			continue

      
        
        123
        +		}

      
        
        124
        +		diags[file] = append(diags[file], s.findToDiagnostic(find))

      
        
        125
        +	}

      
        
        126
        +	return diags

      
        
        127
        +}

      
        
        128
        +

      
        
        129
        +func (s *server) findToDiagnostic(find linter.Find) protocol.Diagnostic {

      
        
        130
        +	return protocol.Diagnostic{

      
        
        131
        +		Range:    spanToRange(find.Span),

      
        
        132
        +		Severity: severityToLSP(find.Severity),

      
        
        133
        +		Message:  protocol.String(find.Message),

      
        
        134
        +		Source:   protocol.NewOptional(s.name),

      
        
        135
        +		Code:     protocol.String(string(find.Code)),

      
        
        136
        +	}

      
        
        137
        +}

      
        
        138
        +

      
        
        139
        +func dedupFinds(finds []linter.Find) []linter.Find {

      
        
        140
        +	seen := make(map[string]bool)

      
        
        141
        +	dedup := make([]linter.Find, 0, len(finds))

      
        
        142
        +	for _, f := range finds {

      
        
        143
        +		s := f.Span.Start

      
        
        144
        +		key := fmt.Sprintf("%s:%d:%d:%s", s.File, s.Line, s.Col, f.Code) // TODO: performace

      
        
        145
        +		if seen[key] {

      
        
        146
        +			continue

      
        
        147
        +		}

      
        
        148
        +		seen[key] = true

      
        
        149
        +		dedup = append(dedup, f)

      
        
        150
        +	}

      
        
        151
        +	return dedup

      
        
        152
        +}

      
        
        153
        +

      
        
        154
        +func spanToRange(span token.Span) protocol.Range {

      
        
        155
        +	return protocol.Range{

      
        
        156
        +		Start: protocol.Position{

      
        
        157
        +			Line:      max(0, uint32(span.Start.Line-1)),

      
        
        158
        +			Character: max(0, uint32(span.Start.Col-1)),

      
        
        159
        +		},

      
        
        160
        +		End: protocol.Position{

      
        
        161
        +			Line:      max(0, uint32(span.End.Line-1)),

      
        
        162
        +			Character: uint32(max(0, span.End.Col-1)),

      
        
        163
        +		},

      
        
        164
        +	}

      
        
        165
        +}

      
        
        166
        +

      
        
        167
        +func severityToLSP(s linter.Severity) protocol.DiagnosticSeverity {

      
        
        168
        +	switch s {

      
        
        169
        +	case linter.SeverityError:

      
        
        170
        +		return protocol.DiagnosticSeverityError

      
        
        171
        +	case linter.SeverityWarning:

      
        
        172
        +		return protocol.DiagnosticSeverityWarning

      
        
        173
        +	case linter.SeverityInfo:

      
        
        174
        +		return protocol.DiagnosticSeverityInformation

      
        
        175
        +	case linter.SeverityHint:

      
        
        176
        +		return protocol.DiagnosticSeverityHint

      
        
        177
        +	default:

      
        
        178
        +		panic("impossible diagnostic severity")

      
        
        179
        +	}

      
        
        180
        +}

      
A internal/lsp/lsp.go
···
        
        1
        +package lsp

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"io"

      
        
        6
        +	"log/slog"

      
        
        7
        +	"os"

      
        
        8
        +

      
        
        9
        +	"go.lsp.dev/jsonrpc2"

      
        
        10
        +	"go.lsp.dev/protocol"

      
        
        11
        +	"go.lsp.dev/uri"

      
        
        12
        +

      
        
        13
        +	"olexsmir.xyz/clerk/internal/linter"

      
        
        14
        +	"olexsmir.xyz/clerk/journal"

      
        
        15
        +)

      
        
        16
        +

      
        
        17
        +type Server struct{ server *server }

      
        
        18
        +

      
        
        19
        +func NewServer(version string) Server {

      
        
        20
        +	return Server{

      
        
        21
        +		&server{

      
        
        22
        +			name:    "clerk",

      
        
        23
        +			version: version,

      
        
        24
        +

      
        
        25
        +			openDocs: make(map[uri.URI]docState),

      
        
        26
        +

      
        
        27
        +			linter: linter.NewLinter(linter.Rules),

      
        
        28
        +			loader: journal.NewLoader(),

      
        
        29
        +

      
        
        30
        +			log: slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{})),

      
        
        31
        +		},

      
        
        32
        +	}

      
        
        33
        +}

      
        
        34
        +

      
        
        35
        +func (s *Server) Run(ctx context.Context, stdin io.ReadCloser, stdout io.WriteCloser) error {

      
        
        36
        +	stream := jsonrpc2.NewStream(readWriterCloser{

      
        
        37
        +		Reader: stdin,

      
        
        38
        +		Writer: stdout,

      
        
        39
        +		Closer: stdin,

      
        
        40
        +	})

      
        
        41
        +

      
        
        42
        +	_, conn, client := protocol.NewServer(ctx, s.server, stream)

      
        
        43
        +	defer conn.Close()

      
        
        44
        +

      
        
        45
        +	s.server.client = client

      
        
        46
        +

      
        
        47
        +	<-conn.Done()

      
        
        48
        +	return conn.Err()

      
        
        49
        +}

      
        
        50
        +

      
        
        51
        +type readWriterCloser struct {

      
        
        52
        +	io.Reader

      
        
        53
        +	io.Writer

      
        
        54
        +	io.Closer

      
        
        55
        +}

      
A internal/lsp/server.go
···
        
        1
        +package lsp

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"log/slog"

      
        
        6
        +	"sync"

      
        
        7
        +	"time"

      
        
        8
        +

      
        
        9
        +	"go.lsp.dev/protocol"

      
        
        10
        +	"go.lsp.dev/uri"

      
        
        11
        +

      
        
        12
        +	"olexsmir.xyz/clerk/internal/linter"

      
        
        13
        +	"olexsmir.xyz/clerk/journal"

      
        
        14
        +	"olexsmir.xyz/clerk/journal/semantic"

      
        
        15
        +)

      
        
        16
        +

      
        
        17
        +type server struct {

      
        
        18
        +	protocol.UnimplementedServer

      
        
        19
        +

      
        
        20
        +	client protocol.Client

      
        
        21
        +	log    *slog.Logger

      
        
        22
        +

      
        
        23
        +	version, name string

      
        
        24
        +

      
        
        25
        +	linter    *linter.Linter

      
        
        26
        +	loader    *journal.Loader

      
        
        27
        +	debouncer *time.Timer

      
        
        28
        +

      
        
        29
        +	mu          sync.Mutex

      
        
        30
        +	openDocs    map[uri.URI]docState

      
        
        31
        +	semanticCtx *semantic.Context

      
        
        32
        +}

      
        
        33
        +

      
        
        34
        +type docState struct {

      
        
        35
        +	text       string

      
        
        36
        +	version    int32

      
        
        37
        +	languageID protocol.LanguageKind

      
        
        38
        +}

      
        
        39
        +

      
        
        40
        +func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {

      
        
        41
        +	return &protocol.InitializeResult{

      
        
        42
        +		ServerInfo: protocol.ServerInfo{

      
        
        43
        +			Name:    s.name,

      
        
        44
        +			Version: protocol.NewOptional(s.version),

      
        
        45
        +		},

      
        
        46
        +		Capabilities: protocol.ServerCapabilities{

      
        
        47
        +			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},

      
        
        48
        +			TextDocumentSync: &protocol.TextDocumentSyncOptions{

      
        
        49
        +				OpenClose: new(true),

      
        
        50
        +				Change:    new(protocol.TextDocumentSyncKindFull),

      
        
        51
        +			},

      
        
        52
        +		},

      
        
        53
        +	}, nil

      
        
        54
        +}

      
        
        55
        +

      
        
        56
        +func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {

      
        
        57
        +	s.scheduleWorkspaceRebuild(ctx)

      
        
        58
        +	return nil

      
        
        59
        +}

      
        
        60
        +

      
        
        61
        +func (s *server) Shutdown(ctx context.Context) error {

      
        
        62
        +	s.cancelWorkspaceRebuild()

      
        
        63
        +	return nil

      
        
        64
        +}

      
        
        65
        +

      
        
        66
        +func (s *server) Exit(ctx context.Context) error {

      
        
        67
        +	return nil

      
        
        68
        +}

      
A internal/lsp/textdocument_sync.go
···
        
        1
        +package lsp

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +

      
        
        6
        +	"go.lsp.dev/protocol"

      
        
        7
        +	"go.lsp.dev/uri"

      
        
        8
        +)

      
        
        9
        +

      
        
        10
        +func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {

      
        
        11
        +	s.openDoc(params.TextDocument.URI, params.TextDocument.Text, params.TextDocument.Version, params.TextDocument.LanguageID)

      
        
        12
        +	s.scheduleWorkspaceRebuild(ctx)

      
        
        13
        +	return nil

      
        
        14
        +}

      
        
        15
        +

      
        
        16
        +func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {

      
        
        17
        +	s.updateDoc(params.TextDocument.URI, params.TextDocument.Version, params.ContentChanges)

      
        
        18
        +	s.scheduleWorkspaceRebuild(ctx)

      
        
        19
        +	return nil

      
        
        20
        +}

      
        
        21
        +

      
        
        22
        +func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {

      
        
        23
        +	s.closeDoc(params.TextDocument.URI)

      
        
        24
        +	s.scheduleWorkspaceRebuild(ctx)

      
        
        25
        +	return nil

      
        
        26
        +}

      
        
        27
        +

      
        
        28
        +func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {

      
        
        29
        +	return nil

      
        
        30
        +}

      
        
        31
        +

      
        
        32
        +// Document management

      
        
        33
        +

      
        
        34
        +func (s *server) openDoc(u uri.URI, text string, version int32, langID protocol.LanguageKind) {

      
        
        35
        +	s.mu.Lock()

      
        
        36
        +	s.openDocs[u] = docState{

      
        
        37
        +		text:       text,

      
        
        38
        +		version:    version,

      
        
        39
        +		languageID: langID,

      
        
        40
        +	}

      
        
        41
        +	s.mu.Unlock()

      
        
        42
        +}

      
        
        43
        +

      
        
        44
        +func (s *server) updateDoc(u uri.URI, version int32, changes []protocol.TextDocumentContentChangeEvent) {

      
        
        45
        +	s.mu.Lock()

      
        
        46
        +	defer s.mu.Unlock()

      
        
        47
        +	state, ok := s.openDocs[u]

      
        
        48
        +	if !ok {

      
        
        49
        +		return

      
        
        50
        +	}

      
        
        51
        +	state.version = version

      
        
        52
        +	for _, ch := range changes {

      
        
        53
        +		switch ev := ch.(type) {

      
        
        54
        +		case *protocol.TextDocumentContentChangeWholeDocument:

      
        
        55
        +			state.text = ev.Text

      
        
        56
        +		case *protocol.TextDocumentContentChangePartial:

      
        
        57
        +			_ = ev // TODO: incremental edit support

      
        
        58
        +		}

      
        
        59
        +	}

      
        
        60
        +	s.openDocs[u] = state

      
        
        61
        +}

      
        
        62
        +

      
        
        63
        +func (s *server) closeDoc(u uri.URI) {

      
        
        64
        +	s.mu.Lock()

      
        
        65
        +	delete(s.openDocs, u)

      
        
        66
        +	s.mu.Unlock()

      
        
        67
        +}

      
        
        68
        +

      
        
        69
        +func (s *server) getDocText(u uri.URI) (string, bool) {

      
        
        70
        +	s.mu.Lock()

      
        
        71
        +	state, ok := s.openDocs[u]

      
        
        72
        +	s.mu.Unlock()

      
        
        73
        +	return state.text, ok

      
        
        74
        +}

      
M journal/loader.go
···
        37
        37
         	return l.loadBytes(path, src, nil)

      
        38
        38
         }

      
        39
        39
         

      
        
        40
        +func (l *Loader) Reload(path string, src []byte) (*ParsedFile, error) {

      
        
        41
        +	abs, err := filepath.Abs(path)

      
        
        42
        +	if err != nil {

      
        
        43
        +		return nil, err

      
        
        44
        +	}

      
        
        45
        +	delete(l.files, abs)

      
        
        46
        +	return l.loadBytes(abs, src, nil)

      
        
        47
        +}

      
        
        48
        +

      
        
        49
        +// Roots returns files not transitively included by any loaded file.

      
        
        50
        +func (l *Loader) Roots() []*ParsedFile {

      
        
        51
        +	included := make(map[string]bool)

      
        
        52
        +	for _, pf := range l.files {

      
        
        53
        +		for _, inc := range pf.Includes {

      
        
        54
        +			included[inc.Path] = true

      
        
        55
        +		}

      
        
        56
        +	}

      
        
        57
        +	var roots []*ParsedFile

      
        
        58
        +	for _, pf := range l.files {

      
        
        59
        +		if !included[pf.Path] {

      
        
        60
        +			roots = append(roots, pf)

      
        
        61
        +		}

      
        
        62
        +	}

      
        
        63
        +	return roots

      
        
        64
        +}

      
        
        65
        +

      
        
        66
        +// CollectFiles returns root and all its transitive includes.

      
        
        67
        +func CollectFiles(root *ParsedFile) []*ParsedFile {

      
        
        68
        +	var files []*ParsedFile

      
        
        69
        +	visited := make(map[*ParsedFile]bool)

      
        
        70
        +	var walk func(*ParsedFile)

      
        
        71
        +	walk = func(pf *ParsedFile) {

      
        
        72
        +		if visited[pf] {

      
        
        73
        +			return

      
        
        74
        +		}

      
        
        75
        +		visited[pf] = true

      
        
        76
        +		files = append(files, pf)

      
        
        77
        +		for _, inc := range pf.Includes {

      
        
        78
        +			walk(inc)

      
        
        79
        +		}

      
        
        80
        +	}

      
        
        81
        +	walk(root)

      
        
        82
        +	return files

      
        
        83
        +}

      
        
        84
        +

      
        40
        85
         // Ordered returns all files in dependency order (included before includer)

      
        41
        86
         func (l *Loader) Ordered() []*ParsedFile {

      
        42
        87
         	visited := make(map[string]bool)