all repos

clerk @ f9a2588e16c2bb5c72fd07cf0e11ad265ffbad86

missing tooling for ledger/hledger
9 files changed, 220 insertions(+), 10 deletions(-)
lsp server
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-11 12:58:15 +0300
Authored at: 2026-06-28 14:55:36 +0300
Change ID: uxpownpksskyorzxoovtvluoroprzqwu
Parent: e62c721
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
        +)

      
        8
        12
         

      
        9
        13
         require (

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

      
        
        15
        +	go.lsp.dev/uri v1.0.1 // 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
···
        75
        75
         				}},

      
        76
        76
         			},

      
        77
        77
         			{

      
        
        78
        +				Name:   "lsp",

      
        
        79
        +				Usage:  "lsp server",

      
        
        80
        +				Action: c.lspAction,

      
        
        81
        +			},

      
        
        82
        +			{

      
        78
        83
         				Name:   "lint",

      
        79
        84
         				Usage:  "lint journal files for common mistakes",

      
        80
        85
         				Action: c.lintAction,

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"os"

      
        
        6
        +

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

      
        
        8
        +

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

      
        
        10
        +)

      
        
        11
        +

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

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

      
        
        14
        +

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

      
        
        16
        +}

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"io"

      
        
        6
        +

      
        
        7
        +	"go.lsp.dev/jsonrpc2"

      
        
        8
        +	"go.lsp.dev/protocol"

      
        
        9
        +	"go.lsp.dev/uri"

      
        
        10
        +

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

      
        
        12
        +	"olexsmir.xyz/clerk/journal/printer"

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

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +type Server struct {

      
        
        17
        +	sema   *semantic.Context

      
        
        18
        +	linter *linter.Linter

      
        
        19
        +

      
        
        20
        +	version string

      
        
        21
        +	server  *server

      
        
        22
        +}

      
        
        23
        +

      
        
        24
        +func NewServer(version string) *Server {

      
        
        25
        +	return &Server{

      
        
        26
        +		// sema:   sema,

      
        
        27
        +		// linter: linter,

      
        
        28
        +		server: &server{

      
        
        29
        +			version:    version,

      
        
        30
        +			printerCfg: printer.DefaultConfig,

      
        
        31
        +			docs:       make(map[uri.URI]docState),

      
        
        32
        +		},

      
        
        33
        +	}

      
        
        34
        +}

      
        
        35
        +

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

      
        
        37
        +	stream := jsonrpc2.NewStream(readWriterCloser{

      
        
        38
        +		Reader: stdin,

      
        
        39
        +		Writer: stdout,

      
        
        40
        +		Closer: stdin,

      
        
        41
        +	})

      
        
        42
        +

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

      
        
        44
        +	defer conn.Close()

      
        
        45
        +

      
        
        46
        +	s.server.client = client

      
        
        47
        +

      
        
        48
        +	<-conn.Done()

      
        
        49
        +	return conn.Err()

      
        
        50
        +}

      
        
        51
        +

      
        
        52
        +type readWriterCloser struct {

      
        
        53
        +	io.Reader

      
        
        54
        +	io.Writer

      
        
        55
        +	io.Closer

      
        
        56
        +}

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"sync"

      
        
        6
        +

      
        
        7
        +	"go.lsp.dev/protocol"

      
        
        8
        +	"go.lsp.dev/uri"

      
        
        9
        +

      
        
        10
        +	"olexsmir.xyz/clerk/journal/printer"

      
        
        11
        +)

      
        
        12
        +

      
        
        13
        +type docState struct {

      
        
        14
        +	text       string

      
        
        15
        +	version    int32

      
        
        16
        +	languageID protocol.LanguageKind

      
        
        17
        +}

      
        
        18
        +

      
        
        19
        +type server struct {

      
        
        20
        +	protocol.UnimplementedServer

      
        
        21
        +

      
        
        22
        +	client  protocol.Client

      
        
        23
        +	version string

      
        
        24
        +

      
        
        25
        +	printerCfg *printer.Config

      
        
        26
        +

      
        
        27
        +	mu   sync.RWMutex

      
        
        28
        +	docs map[uri.URI]docState

      
        
        29
        +}

      
        
        30
        +

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

      
        
        32
        +	return &protocol.InitializeResult{

      
        
        33
        +		ServerInfo: protocol.ServerInfo{

      
        
        34
        +			Name:    "clerk",

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

      
        
        36
        +		},

      
        
        37
        +		Capabilities: protocol.ServerCapabilities{

      
        
        38
        +			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},

      
        
        39
        +			TextDocumentSync: &protocol.TextDocumentSyncOptions{

      
        
        40
        +				OpenClose: new(true),

      
        
        41
        +				Change:    new(protocol.TextDocumentSyncKindFull), // TODO: Incremental

      
        
        42
        +			},

      
        
        43
        +			// TODO: diagnostics provider

      
        
        44
        +			// TODO: completion

      
        
        45
        +		},

      
        
        46
        +	}, nil

      
        
        47
        +}

      
        
        48
        +

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

      
        
        50
        +	return nil

      
        
        51
        +}

      
        
        52
        +

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

      
        
        54
        +	return nil

      
        
        55
        +}

      
        
        56
        +

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

      
        
        58
        +	return nil

      
        
        59
        +}

      
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.mu.Lock()

      
        
        12
        +	s.docs[params.TextDocument.URI] = docState{

      
        
        13
        +		text:       params.TextDocument.Text,

      
        
        14
        +		version:    params.TextDocument.Version,

      
        
        15
        +		languageID: params.TextDocument.LanguageID,

      
        
        16
        +	}

      
        
        17
        +	s.mu.Unlock()

      
        
        18
        +	return nil

      
        
        19
        +}

      
        
        20
        +

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

      
        
        22
        +	s.mu.Lock()

      
        
        23
        +	defer s.mu.Unlock()

      
        
        24
        +

      
        
        25
        +	state, ok := s.docs[params.TextDocument.URI]

      
        
        26
        +	if !ok {

      
        
        27
        +		return nil

      
        
        28
        +	}

      
        
        29
        +

      
        
        30
        +	state.version = params.TextDocument.Version

      
        
        31
        +	for _, ch := range params.ContentChanges {

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

      
        
        33
        +		case *protocol.TextDocumentContentChangeWholeDocument:

      
        
        34
        +			state.text = ev.Text

      
        
        35
        +		case *protocol.TextDocumentContentChangePartial:

      
        
        36
        +			// TODO: incremental edit — apply range replacement

      
        
        37
        +			_ = ev

      
        
        38
        +		}

      
        
        39
        +	}

      
        
        40
        +	s.docs[params.TextDocument.URI] = state

      
        
        41
        +	return nil

      
        
        42
        +}

      
        
        43
        +

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

      
        
        45
        +	s.mu.Lock()

      
        
        46
        +	defer s.mu.Unlock()

      
        
        47
        +	delete(s.docs, params.TextDocument.URI)

      
        
        48
        +	return nil

      
        
        49
        +}

      
        
        50
        +

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

      
        
        52
        +	return nil

      
        
        53
        +}

      
        
        54
        +

      
        
        55
        +func (s *server) GetText(uri uri.URI) (string, bool) {

      
        
        56
        +	s.mu.RLock()

      
        
        57
        +	defer s.mu.RUnlock()

      
        
        58
        +	state, ok := s.docs[uri]

      
        
        59
        +	return state.text, ok

      
        
        60
        +}

      
M journal/printer/printer.go
···
        34
        34
         	CommodityPos       CommodityPos // where to place commodity

      
        35
        35
         }

      
        36
        36
         

      
        37
        
        -var defaultConfig = &Config{

      
        
        37
        +var DefaultConfig = &Config{

      
        38
        38
         	TabIndent:          false,

      
        39
        39
         	IndentWidth:        2,

      
        40
        40
         	PreserveBlankLines: false,

      ···
        63
        63
         }

      
        64
        64
         

      
        65
        65
         // Fprint formats using the default config.

      
        66
        
        -func Fprint(w io.Writer, j *ast.Journal) error { return defaultConfig.Fprint(w, j) }

      
        
        66
        +func Fprint(w io.Writer, j *ast.Journal) error { return DefaultConfig.Fprint(w, j) }

      
        67
        67
         

      
        68
        68
         // Fprint formats a parsed journal.

      
        69
        69
         func (c *Config) Fprint(w io.Writer, j *ast.Journal) error {

      
        70
        70
         	if c == nil {

      
        71
        
        -		c = defaultConfig

      
        
        71
        +		c = DefaultConfig

      
        72
        72
         	}

      
        73
        73
         	p := printer{cfg: c, indent: c.indent()}

      
        74
        74
         

      ···
        83
        83
         }

      
        84
        84
         

      
        85
        85
         // FprintEntry formats a single ast entry using the default config.

      
        86
        
        -func FprintEntry(w io.Writer, e ast.Entry) error { return defaultConfig.FprintEntry(w, e) }

      
        
        86
        +func FprintEntry(w io.Writer, e ast.Entry) error { return DefaultConfig.FprintEntry(w, e) }

      
        87
        87
         

      
        88
        88
         // FprintEntry formats a single journal entry.

      
        89
        89
         func (c *Config) FprintEntry(w io.Writer, e ast.Entry) error {

      
        90
        90
         	if c == nil {

      
        91
        
        -		c = defaultConfig

      
        
        91
        +		c = DefaultConfig

      
        92
        92
         	}

      
        93
        93
         	p := printer{cfg: c, indent: c.indent()}

      
        94
        94
         	p.formatEntry(e)

      
M journal/printer/printer_test.go
···
        20
        20
         				t.Fatal(err)

      
        21
        21
         			}

      
        22
        22
         			var b strings.Builder

      
        23
        
        -			if err := defaultConfig.Fprint(&b, pf.Ast); err != nil {

      
        
        23
        +			if err := DefaultConfig.Fprint(&b, pf.Ast); err != nil {

      
        24
        24
         				t.Fatal(err)

      
        25
        25
         			}

      
        26
        26
         

      ···
        41
        41
         			b.ResetTimer()

      
        42
        42
         			for i := 0; i < b.N; i++ {

      
        43
        43
         				var buf strings.Builder

      
        44
        
        -				if err := defaultConfig.Fprint(&buf, pf.Ast); err != nil {

      
        
        44
        +				if err := DefaultConfig.Fprint(&buf, pf.Ast); err != nil {

      
        45
        45
         					b.Fatal(err)

      
        46
        46
         				}

      
        47
        47
         			}