all repos

clerk @ 624171a

missing tooling for ledger/hledger
5 files changed, 85 insertions(+), 9 deletions(-)
lsp: add formatter
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-12 13:17:30 +0300
Authored at: 2026-07-11 13:35:22 +0300
Change ID: uowsszmkskwqvokqzoxqpwnurkvwwlnw
Parent: 3d92c0e
M internal/lsp/lsp.go
···
        12
        12
         

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

      
        14
        14
         	"olexsmir.xyz/clerk/journal"

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

      
        15
        16
         )

      
        16
        17
         

      
        17
        18
         type Server struct{ server *server }

      ···
        24
        25
         

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

      
        26
        27
         

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

      
        28
        
        -			loader: journal.NewLoader(),

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

      
        
        29
        +			loader:  journal.NewLoader(),

      
        
        30
        +			printer: printer.DefaultConfig,

      
        29
        31
         

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

      
        31
        33
         		},

      
M internal/lsp/server.go
···
        11
        11
         

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

      
        13
        13
         	"olexsmir.xyz/clerk/journal"

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

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

      
        15
        16
         )

      
        16
        17
         

      ···
        24
        25
         

      
        25
        26
         	linter    *linter.Linter

      
        26
        27
         	loader    *journal.Loader

      
        
        28
        +	printer   *printer.Config

      
        27
        29
         	debouncer *time.Timer

      
        28
        30
         

      
        29
        31
         	mu          sync.Mutex

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"strings"

      
        
        7
        +

      
        
        8
        +	"go.lsp.dev/protocol"

      
        
        9
        +	"olexsmir.xyz/clerk/journal/lexer"

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

      
        
        11
        +)

      
        
        12
        +

      
        
        13
        +func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) {

      
        
        14
        +	text, ok := s.getDocText(params.TextDocument.URI)

      
        
        15
        +	if !ok {

      
        
        16
        +		return nil, nil

      
        
        17
        +	}

      
        
        18
        +

      
        
        19
        +	lex := lexer.New(params.TextDocument.URI.Path(), []byte(text))

      
        
        20
        +	jnrl := parser.New(lex).ParseJournal()

      
        
        21
        +	if len(jnrl.Errors) > 0 {

      
        
        22
        +		return nil, fmt.Errorf("can't format file with errors: %v", jnrl.Errors[0].Message) // TODO: report all errors

      
        
        23
        +	}

      
        
        24
        +

      
        
        25
        +	var buf strings.Builder

      
        
        26
        +	if err := s.printer.Fprint(&buf, jnrl); err != nil {

      
        
        27
        +		return nil, fmt.Errorf("format: %w", err)

      
        
        28
        +	}

      
        
        29
        +

      
        
        30
        +	newText := buf.String()

      
        
        31
        +	if newText == text {

      
        
        32
        +		return nil, nil

      
        
        33
        +	}

      
        
        34
        +

      
        
        35
        +	endLine, endChar := getEndLineAndChar(text)

      
        
        36
        +	return []protocol.TextEdit{{

      
        
        37
        +		NewText: newText,

      
        
        38
        +		Range: protocol.Range{

      
        
        39
        +			Start: protocol.Position{

      
        
        40
        +				Line:      0,

      
        
        41
        +				Character: 0,

      
        
        42
        +			},

      
        
        43
        +			End: protocol.Position{

      
        
        44
        +				Line:      uint32(endLine),

      
        
        45
        +				Character: uint32(endChar),

      
        
        46
        +			},

      
        
        47
        +		},

      
        
        48
        +	}}, nil

      
        
        49
        +}

      
        
        50
        +

      
        
        51
        +func getEndLineAndChar(txt string) (int, int) {

      
        
        52
        +	endLine := 0

      
        
        53
        +	lastNewline := -1

      
        
        54
        +	for i, ch := range txt {

      
        
        55
        +		if ch == '\n' {

      
        
        56
        +			endLine++

      
        
        57
        +			lastNewline = i

      
        
        58
        +		}

      
        
        59
        +	}

      
        
        60
        +	endChar := 0

      
        
        61
        +	if lastNewline >= 0 {

      
        
        62
        +		if lastNewline == len(txt)-1 {

      
        
        63
        +			endLine++

      
        
        64
        +			endChar = 0

      
        
        65
        +		} else {

      
        
        66
        +			endChar = len(txt) - lastNewline - 1

      
        
        67
        +		}

      
        
        68
        +	} else {

      
        
        69
        +		endChar = len(txt)

      
        
        70
        +	}

      
        
        71
        +	return endLine, endChar

      
        
        72
        +}

      
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
         			}