all repos

clerk @ 03eda2a2e8694ad823182b6c81023d17e91b2385

missing tooling for ledger/hledger
10 files changed, 184 insertions(+), 201 deletions(-)
lsp: move code around
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-03 12:52:15 +0300
Authored at: 2026-08-02 22:37:01 +0300
Change ID: ylptywswrnunsulykuuvtzukznzynkwq
Parent: d7328be
M internal/linter/rules.go
···
        29
        29
         	&UndeclaredCommodity{},

      
        30
        30
         	&UndeclaredAccount{},

      
        31
        31
         	&UnbalancedTransaction{},

      
        32
        
        -	// &UnusedAccount{},

      
        
        32
        +	&UnusedAccount{},

      
        33
        33
         }

      
M internal/lsp/config.go
···
        16
        16
         	SemanticHighlighting: true,

      
        17
        17
         }

      
        18
        18
         

      
        19
        
        -func (c *Config) merge(v protocol.LSPAny) {

      
        
        19
        +func (c *Config) merge(v protocol.LSPAny) error {

      
        20
        20
         	var patch struct {

      
        21
        21
         		SemanticHighlighting *bool `json:"semantic_highlighting,case:ignore"`

      
        22
        22
         	}

      
        23
        23
         

      
        24
        24
         	if err := json.Unmarshal(v, &patch); err != nil {

      
        25
        
        -		return

      
        
        25
        +		return err

      
        26
        26
         	}

      
        27
        27
         	if patch.SemanticHighlighting != nil {

      
        28
        28
         		c.SemanticHighlighting = *patch.SemanticHighlighting

      
        29
        29
         	}

      
        
        30
        +	return nil

      
        30
        31
         }

      
        31
        32
         

      
        32
        33
         func (s *server) semanticHighlightingEnabled() bool {

      
M internal/lsp/config_test.go
···
        6
        6
         	"go.lsp.dev/protocol"

      
        7
        7
         )

      
        8
        8
         

      
        9
        
        -func TestConfigMerge(t *testing.T) {

      
        
        9
        +func TestConfig_Merge(t *testing.T) {

      
        10
        10
         	tests := map[string]struct {

      
        11
        
        -		cfg  Config

      
        12
        
        -		in   string

      
        13
        
        -		want Config

      
        
        11
        +		in        string

      
        
        12
        +		cfg, want Config

      
        14
        13
         	}{

      
        15
        
        -		"if not set, uses default":    {Config{}, `{"semanticHighlighting": true}`, Config{SemanticHighlighting: true}},

      
        16
        
        -		"empty object keeps defaults": {DefaultConfig, `{}`, Config{SemanticHighlighting: true}},

      
        17
        
        -		"nil settings keep defaults":  {DefaultConfig, "", Config{SemanticHighlighting: true}},

      
        
        14
        +		"if not set, uses default":    {`{"semanticHighlighting": true}`, Config{}, Config{SemanticHighlighting: true}},

      
        
        15
        +		"empty object keeps defaults": {`{}`, DefaultConfig, Config{SemanticHighlighting: true}},

      
        
        16
        +		"nil settings keep defaults":  {"", DefaultConfig, Config{SemanticHighlighting: true}},

      
        18
        17
         

      
        19
        
        -		"disables provided option": {DefaultConfig, `{"semanticHighlighting": false}`, Config{SemanticHighlighting: false}},

      
        20
        
        -		"snake_case key":           {DefaultConfig, `{"semantic_highlighting": false}`, Config{SemanticHighlighting: false}},

      
        21
        
        -		"kebab-case key":           {DefaultConfig, `{"semantic-highlighting": false}`, Config{SemanticHighlighting: false}},

      
        
        18
        +		"disables provided option": {`{"semanticHighlighting": false}`, DefaultConfig, Config{SemanticHighlighting: false}},

      
        
        19
        +		"snake_case key":           {`{"semantic_highlighting": false}`, DefaultConfig, Config{SemanticHighlighting: false}},

      
        
        20
        +		"kebab-case key":           {`{"semantic-highlighting": false}`, DefaultConfig, Config{SemanticHighlighting: false}},

      
        22
        21
         

      
        23
        
        -		"malformed settings ignored":  {Config{SemanticHighlighting: true}, `{`, Config{SemanticHighlighting: true}},

      
        24
        
        -		"non-object settings ignored": {Config{SemanticHighlighting: true}, `"clerk"`, Config{SemanticHighlighting: true}},

      
        25
        
        -		"unknown fields ignored":      {Config{SemanticHighlighting: true}, `{"lint": true}`, Config{SemanticHighlighting: true}},

      
        
        22
        +		"malformed settings ignored":  {`{`, DefaultConfig, DefaultConfig},

      
        
        23
        +		"non-object settings ignored": {`"clerk"`, DefaultConfig, DefaultConfig},

      
        
        24
        +		"unknown fields ignored":      {`{"lint": true}`, DefaultConfig, DefaultConfig},

      
        26
        25
         	}

      
        27
        26
         	for name, tt := range tests {

      
        28
        27
         		t.Run(name, func(t *testing.T) {

      ···
        34
        33
         	}

      
        35
        34
         }

      
        36
        35
         

      
        37
        
        -func TestInitialize_AppliesInitializationOptions(t *testing.T) {

      
        
        36
        +func TestServer_Intialize_config(t *testing.T) {

      
        38
        37
         	srv := NewServer("test")

      
        39
        38
         	res, err := srv.server.Initialize(t.Context(), &protocol.InitializeParams{

      
        40
        39
         		InitializationOptions: protocol.LSPAny(`{"semanticHighlighting": false}`),

      
M internal/lsp/diagnostics.go
···
        16
        16
         const diagDebounce = 200 * time.Millisecond

      
        17
        17
         

      
        18
        18
         func (s *server) scheduleDiagnostics(ctx context.Context) {

      
        19
        
        -	s.diagMu.Lock()

      
        20
        
        -	defer s.diagMu.Unlock()

      
        
        19
        +	s.mu.Lock()

      
        
        20
        +	defer s.mu.Unlock()

      
        21
        21
         

      
        22
        22
         	if s.diagCancel != nil {

      
        23
        23
         		s.diagCancel()

      
M internal/lsp/server.go
···
        27
        27
         	loader  *journal.Loader

      
        28
        28
         	printer *printer.Config

      
        29
        29
         

      
        30
        
        -	mu       sync.Mutex

      
        31
        
        -	openDocs map[uri.URI]docState

      
        32
        
        -	current  *analyzer.Analysis

      
        
        30
        +	mu         sync.Mutex

      
        
        31
        +	openDocs   map[uri.URI]docState

      
        
        32
        +	current    *analyzer.Analysis

      
        
        33
        +	diagCancel context.CancelFunc

      
        33
        34
         

      
        34
        35
         	cfgMu  sync.RWMutex

      
        35
        36
         	config Config

      
        36
        
        -

      
        37
        
        -	diagMu     sync.Mutex

      
        38
        
        -	diagCancel context.CancelFunc

      
        39
        37
         }

      
        40
        38
         

      
        41
        39
         func (s *server) analysis() *analyzer.Analysis {

      ···
        62
        60
         		}

      
        63
        61
         	}

      
        64
        62
         	return a

      
        65
        
        -}

      
        66
        
        -

      
        67
        
        -type docState struct {

      
        68
        
        -	text       string

      
        69
        
        -	version    int32

      
        70
        
        -	languageID protocol.LanguageKind

      
        71
        
        -	semTokens  []semanticToken // cached semantic tokens

      
        72
        63
         }

      
        73
        64
         

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

      ···
        116
        107
         

      
        117
        108
         func (s *server) applySettings(v protocol.LSPAny) {

      
        118
        109
         	s.cfgMu.Lock()

      
        119
        
        -	s.config.merge(v)

      
        
        110
        +	if err := s.config.merge(v); err != nil {

      
        
        111
        +		s.log.Error("failed to merge config", "err", err)

      
        
        112
        +	}

      
        120
        113
         	s.cfgMu.Unlock()

      
        121
        114
         }

      
M internal/lsp/textdocument_format.go
···
        12
        12
         )

      
        13
        13
         

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

      
        15
        
        -	text, ok := s.getDocText(params.TextDocument.URI)

      
        
        15
        +	state, ok := s.getDocState(params.TextDocument.URI)

      
        16
        16
         	if !ok {

      
        17
        17
         		return nil, nil

      
        18
        18
         	}

      
        19
        19
         

      
        
        20
        +	text := state.text

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

      
        21
        22
         	jnrl := parser.New(lex).ParseJournal()

      
        22
        23
         	if len(jnrl.Errors) > 0 {

      
M internal/lsp/textdocument_semantic.go
···
        64
        64
         // Implementation

      
        65
        65
         

      
        66
        66
         const (

      
        67
        
        -	SemanticDirective = iota

      
        68
        
        -	SemanticDate

      
        69
        
        -	SemanticAccount

      
        70
        
        -	SemanticCommodity

      
        71
        
        -	SemanticAmount

      
        72
        
        -	SemanticStatus

      
        73
        
        -	SemanticComment

      
        74
        
        -	SemString

      
        75
        
        -	SemOperator

      
        76
        
        -	SemProperty

      
        77
        
        -

      
        78
        
        -	semTypeCount // used to ensure we dont go out of bound

      
        
        67
        +	semDirective = iota

      
        
        68
        +	semDate

      
        
        69
        +	semAccount

      
        
        70
        +	semCommodity

      
        
        71
        +	semAmount

      
        
        72
        +	semStatus

      
        
        73
        +	semComment

      
        
        74
        +	semString

      
        
        75
        +	semOperator

      
        
        76
        +	semProperty

      
        79
        77
         )

      
        80
        78
         

      
        81
        79
         var tokenTypeStrings = []string{

      ···
        109
        107
         }

      
        110
        108
         

      
        111
        109
         type semanticToken struct {

      
        112
        
        -	line      uint32 // 0-based

      
        113
        
        -	col       uint32 // 0-based UTF-16

      
        114
        
        -	length    uint32 // UTF-16 code units

      
        
        110
        +	line, col uint32 // 0-based

      
        
        111
        +	length    uint32

      
        115
        112
         	tokenType uint32

      
        116
        113
         	modifiers uint32

      
        117
        114
         }

      ···
        204
        201
         	case *ast.AutomatedTransaction:

      
        205
        202
         		visitAutomatedTransaction(content, e, emit)

      
        206
        203
         	case *ast.AccountDirective:

      
        207
        
        -		emit(directiveKeyword(e.Span, "account"), SemanticDirective, 0)

      
        208
        
        -		emit(e.Account.Span, SemanticAccount, 0)

      
        
        204
        +		emit(directiveKeyword(e.Span, "account"), semDirective, 0)

      
        
        205
        +		emit(e.Account.Span, semAccount, 0)

      
        209
        206
         		if e.Comment != nil {

      
        210
        
        -			emit(e.Comment.Span, SemanticComment, 0)

      
        
        207
        +			emit(e.Comment.Span, semComment, 0)

      
        211
        208
         		}

      
        212
        209
         	case *ast.CommodityDirective:

      
        213
        
        -		emit(directiveKeyword(e.Span, "commodity"), SemanticDirective, 0)

      
        
        210
        +		emit(directiveKeyword(e.Span, "commodity"), semDirective, 0)

      
        214
        211
         		if e.Format.Span.End.Offset > 0 {

      
        215
        212
         			semEmitAmount(content, &e.Format, emit)

      
        216
        213
         		} else if e.CommoditySpan.Start.Offset > 0 && e.CommoditySpan.End.Offset > 0 {

      
        217
        
        -			emit(e.CommoditySpan, SemanticCommodity, 0)

      
        
        214
        +			emit(e.CommoditySpan, semCommodity, 0)

      
        218
        215
         		}

      
        219
        216
         		if e.Comment != nil {

      
        220
        
        -			emit(e.Comment.Span, SemanticComment, 0)

      
        
        217
        +			emit(e.Comment.Span, semComment, 0)

      
        221
        218
         		}

      
        222
        219
         	case *ast.IncludeDirective:

      
        223
        
        -		emitDirective(content, e.Span, len("include"), SemString, e.Comment, emit)

      
        
        220
        +		emitDirective(content, e.Span, len("include"), semString, e.Comment, emit)

      
        224
        221
         	case *ast.PayeeDirective:

      
        225
        
        -		emitDirective(content, e.Span, len("payee"), SemProperty, e.Comment, emit)

      
        
        222
        +		emitDirective(content, e.Span, len("payee"), semProperty, e.Comment, emit)

      
        226
        223
         	case *ast.TagDirective:

      
        227
        
        -		emitDirective(content, e.Span, len("tag"), SemProperty, e.Comment, emit)

      
        
        224
        +		emitDirective(content, e.Span, len("tag"), semProperty, e.Comment, emit)

      
        228
        225
         	case *ast.AliasDirective:

      
        229
        
        -		emit(directiveKeyword(e.Span, "alias"), SemanticDirective, 0)

      
        230
        
        -		emit(e.From.Span, SemanticAccount, 0)

      
        
        226
        +		emit(directiveKeyword(e.Span, "alias"), semDirective, 0)

      
        
        227
        +		emit(e.From.Span, semAccount, 0)

      
        231
        228
         		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {

      
        232
        
        -			emit(op, SemOperator, 0)

      
        
        229
        +			emit(op, semOperator, 0)

      
        233
        230
         		}

      
        234
        
        -		emit(e.To.Span, SemanticAccount, 0)

      
        
        231
        +		emit(e.To.Span, semAccount, 0)

      
        235
        232
         		if e.Comment != nil {

      
        236
        
        -			emit(e.Comment.Span, SemanticComment, 0)

      
        
        233
        +			emit(e.Comment.Span, semComment, 0)

      
        237
        234
         		}

      
        238
        235
         	case *ast.YearDirective:

      
        239
        236
         		kwLen := len("year")

      
        240
        237
         		if content[e.Span.Start.Offset] == 'Y' {

      
        241
        238
         			kwLen = 1

      
        242
        239
         		}

      
        243
        
        -		emitDirective(content, e.Span, kwLen, SemProperty, e.Comment, emit)

      
        
        240
        +		emitDirective(content, e.Span, kwLen, semProperty, e.Comment, emit)

      
        244
        241
         	case *ast.DecimalMarkDirective:

      
        245
        
        -		emitDirective(content, e.Span, len("decimal-mark"), SemProperty, e.Comment, emit)

      
        
        242
        +		emitDirective(content, e.Span, len("decimal-mark"), semProperty, e.Comment, emit)

      
        246
        243
         	case *ast.DefaultCommodityDirective:

      
        247
        
        -		emit(directiveKeyword(e.Span, "D"), SemanticDirective, 0)

      
        
        244
        +		emit(directiveKeyword(e.Span, "D"), semDirective, 0)

      
        248
        245
         		semEmitAmount(content, &e.Amount, emit)

      
        249
        246
         		if e.Comment != nil {

      
        250
        
        -			emit(e.Comment.Span, SemanticComment, 0)

      
        
        247
        +			emit(e.Comment.Span, semComment, 0)

      
        251
        248
         		}

      
        252
        249
         	case *ast.MarketPriceDirective:

      
        253
        
        -		emit(directiveKeyword(e.Span, "P"), SemanticDirective, 0)

      
        254
        
        -		emit(e.DateTime.Date.Span, SemanticDate, 0)

      
        
        250
        +		emit(directiveKeyword(e.Span, "P"), semDirective, 0)

      
        
        251
        +		emit(e.DateTime.Date.Span, semDate, 0)

      
        255
        252
         		if e.DateTime.Time != nil {

      
        256
        
        -			emit(e.DateTime.Time.Span, SemanticDate, 0)

      
        
        253
        +			emit(e.DateTime.Time.Span, semDate, 0)

      
        257
        254
         		}

      
        258
        255
         		// commodity: text between the date (or time) and the amount

      
        259
        256
         		commStart := e.DateTime.Date.Span.End.Offset

      ···
        261
        258
         			commStart = e.DateTime.Time.Span.End.Offset

      
        262
        259
         		}

      
        263
        260
         		if comm, ok := betweenSpan(content, e.Span.Start.File, commStart, e.Amount.Span.Start.Offset); ok {

      
        264
        
        -			emit(comm, SemanticCommodity, 0)

      
        
        261
        +			emit(comm, semCommodity, 0)

      
        265
        262
         		}

      
        266
        263
         		semEmitAmount(content, &e.Amount, emit)

      
        267
        264
         		if e.Comment != nil {

      
        268
        
        -			emit(e.Comment.Span, SemanticComment, 0)

      
        
        265
        +			emit(e.Comment.Span, semComment, 0)

      
        269
        266
         		}

      
        270
        267
         	case *ast.ConversionDirective:

      
        271
        
        -		emit(directiveKeyword(e.Span, "C"), SemanticDirective, 0)

      
        
        268
        +		emit(directiveKeyword(e.Span, "C"), semDirective, 0)

      
        272
        269
         		semEmitAmount(content, &e.From, emit)

      
        273
        270
         		// = operator: text between the two amounts

      
        274
        271
         		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {

      
        275
        
        -			emit(op, SemOperator, 0)

      
        
        272
        +			emit(op, semOperator, 0)

      
        276
        273
         		}

      
        277
        274
         		semEmitAmount(content, &e.To, emit)

      
        278
        275
         		if e.Comment != nil {

      
        279
        
        -			emit(e.Comment.Span, SemanticComment, 0)

      
        
        276
        +			emit(e.Comment.Span, semComment, 0)

      
        280
        277
         		}

      
        281
        278
         	case *ast.Comment:

      
        282
        
        -		emit(e.Span, SemanticComment, 0)

      
        
        279
        +		emit(e.Span, semComment, 0)

      
        283
        280
         	case *ast.CommentBlockDirective:

      
        284
        
        -		emit(e.Span, SemanticComment, 0)

      
        
        281
        +		emit(e.Span, semComment, 0)

      
        285
        282
         	case *ast.IgnoredDirective:

      
        286
        
        -		emitDirective(content, e.Span, len("N"), SemProperty, e.Comment, emit)

      
        
        283
        +		emitDirective(content, e.Span, len("N"), semProperty, e.Comment, emit)

      
        287
        284
         	case *ast.ApplyDirective:

      
        288
        
        -		emitDirective(content, e.Span, len("apply"), SemProperty, e.Comment, emit)

      
        
        285
        +		emitDirective(content, e.Span, len("apply"), semProperty, e.Comment, emit)

      
        289
        286
         	case *ast.EndDirective:

      
        290
        
        -		emitDirective(content, e.Span, len("end"), SemProperty, e.Comment, emit)

      
        
        287
        +		emitDirective(content, e.Span, len("end"), semProperty, e.Comment, emit)

      
        291
        288
         	case *ast.BlankLine:

      
        292
        289
         	}

      
        293
        290
         }

      
        294
        291
         

      
        295
        292
         func visitTransaction(content string, t *ast.Transaction, emit semEmitFn) {

      
        296
        
        -	emit(t.Date.Span, SemanticDate, 0)

      
        
        293
        +	emit(t.Date.Span, semDate, 0)

      
        297
        294
         	if t.SecondDate != nil {

      
        298
        
        -		emit(t.SecondDate.Span, SemanticDate, 0)

      
        
        295
        +		emit(t.SecondDate.Span, semDate, 0)

      
        299
        296
         	}

      
        300
        297
         	if t.Status.Value != ast.StatusNone {

      
        301
        
        -		emit(t.Status.Span, SemanticStatus, 0)

      
        
        298
        +		emit(t.Status.Span, semStatus, 0)

      
        302
        299
         	}

      
        303
        300
         	if t.Code != nil {

      
        304
        
        -		emit(t.Code.Span, SemString, 0)

      
        
        301
        +		emit(t.Code.Span, semString, 0)

      
        305
        302
         	}

      
        306
        303
         	if t.Payee != nil {

      
        307
        
        -		emit(t.Payee.Span, SemProperty, 0)

      
        
        304
        +		emit(t.Payee.Span, semProperty, 0)

      
        308
        305
         	}

      
        309
        306
         	if t.Note != nil {

      
        310
        
        -		emit(t.Note.Span, SemProperty, 0)

      
        
        307
        +		emit(t.Note.Span, semProperty, 0)

      
        311
        308
         	}

      
        312
        309
         	if t.Comment != nil {

      
        313
        
        -		emit(t.Comment.Span, SemanticComment, 0)

      
        
        310
        +		emit(t.Comment.Span, semComment, 0)

      
        314
        311
         	}

      
        315
        312
         	for i := range t.HeaderComments {

      
        316
        
        -		emit(t.HeaderComments[i].Span, SemanticComment, 0)

      
        
        313
        +		emit(t.HeaderComments[i].Span, semComment, 0)

      
        317
        314
         	}

      
        318
        315
         	for _, p := range t.Postings {

      
        319
        316
         		visitPosting(content, p, emit)

      ···
        322
        319
         

      
        323
        320
         func visitPeriodicTransaction(content string, pt *ast.PeriodicTransaction, emit semEmitFn) {

      
        324
        321
         	// ~ operator is at the start of the period span

      
        325
        
        -	emit(offsetSpan(pt.Span.Start.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), SemOperator, 0)

      
        
        322
        +	emit(offsetSpan(pt.Span.Start.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), semOperator, 0)

      
        326
        323
         

      
        327
        324
         	// The period span covers the whole expr, including any "from ... to ..." dates

      
        328
        325
         	if pt.Period.Span.End.Offset > pt.Period.Span.Start.Offset {

      ···
        336
        333
         		pos := pt.Period.Span.Start.Offset

      
        337
        334
         		for _, d := range dates {

      
        338
        335
         			if d.Span.Start.Offset > pos {

      
        339
        
        -				emit(offsetSpan(pt.Period.Span.Start.File, pos, d.Span.Start.Offset), SemProperty, 0)

      
        
        336
        +				emit(offsetSpan(pt.Period.Span.Start.File, pos, d.Span.Start.Offset), semProperty, 0)

      
        340
        337
         			}

      
        341
        
        -			emit(d.Span, SemanticDate, 0)

      
        
        338
        +			emit(d.Span, semDate, 0)

      
        342
        339
         			pos = d.Span.End.Offset

      
        343
        340
         		}

      
        344
        341
         		if pos < pt.Period.Span.End.Offset {

      
        345
        
        -			emit(offsetSpan(pt.Period.Span.Start.File, pos, pt.Period.Span.End.Offset), SemProperty, 0)

      
        
        342
        +			emit(offsetSpan(pt.Period.Span.Start.File, pos, pt.Period.Span.End.Offset), semProperty, 0)

      
        346
        343
         		}

      
        347
        344
         	}

      
        348
        345
         	if pt.Description != nil {

      
        349
        
        -		emit(pt.Description.Span, SemProperty, 0)

      
        
        346
        +		emit(pt.Description.Span, semProperty, 0)

      
        350
        347
         	}

      
        351
        348
         	if pt.Comment != nil {

      
        352
        
        -		emit(pt.Comment.Span, SemanticComment, 0)

      
        
        349
        +		emit(pt.Comment.Span, semComment, 0)

      
        353
        350
         	}

      
        354
        351
         	for i := range pt.HeaderComments {

      
        355
        
        -		emit(pt.HeaderComments[i].Span, SemanticComment, 0)

      
        
        352
        +		emit(pt.HeaderComments[i].Span, semComment, 0)

      
        356
        353
         	}

      
        357
        354
         	for _, p := range pt.Postings {

      
        358
        355
         		visitPosting(content, p, emit)

      ···
        361
        358
         

      
        362
        359
         func visitAutomatedTransaction(content string, at *ast.AutomatedTransaction, emit semEmitFn) {

      
        363
        360
         	// = operator is at the start of the expression span

      
        364
        
        -	emit(offsetSpan(at.Span.Start.File, at.Span.Start.Offset, at.Span.Start.Offset+1), SemOperator, 0)

      
        
        361
        +	emit(offsetSpan(at.Span.Start.File, at.Span.Start.Offset, at.Span.Start.Offset+1), semOperator, 0)

      
        365
        362
         

      
        366
        363
         	if at.Expr.Value != "" {

      
        367
        
        -		emit(at.Expr.Span, SemString, 0)

      
        
        364
        +		emit(at.Expr.Span, semString, 0)

      
        368
        365
         	}

      
        369
        366
         	if at.Comment != nil {

      
        370
        
        -		emit(at.Comment.Span, SemanticComment, 0)

      
        
        367
        +		emit(at.Comment.Span, semComment, 0)

      
        371
        368
         	}

      
        372
        369
         	for i := range at.HeaderComments {

      
        373
        
        -		emit(at.HeaderComments[i].Span, SemanticComment, 0)

      
        
        370
        +		emit(at.HeaderComments[i].Span, semComment, 0)

      
        374
        371
         	}

      
        375
        372
         	for _, p := range at.Postings {

      
        376
        373
         		visitPosting(content, p, emit)

      ···
        379
        376
         

      
        380
        377
         func visitPosting(content string, p *ast.Posting, emit semEmitFn) {

      
        381
        378
         	if p.Status.Value != ast.StatusNone {

      
        382
        
        -		emit(p.Status.Span, SemanticStatus, 0)

      
        
        379
        +		emit(p.Status.Span, semStatus, 0)

      
        383
        380
         	}

      
        384
        381
         

      
        385
        382
         	// virtual brackets

      ···
        388
        385
         		for off := p.Span.Start.Offset; off < p.Account.Span.Start.Offset && off < p.Span.End.Offset; off++ {

      
        389
        386
         			if content[off] == '(' || content[off] == '[' {

      
        390
        387
         				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}

      
        391
        
        -				emit(brSpan, SemOperator, modifierAbstract)

      
        
        388
        +				emit(brSpan, semOperator, modifierAbstract)

      
        392
        389
         				break

      
        393
        390
         			}

      
        394
        391
         		}

      ···
        396
        393
         		for off := p.Account.Span.End.Offset; off < p.Span.End.Offset; off++ {

      
        397
        394
         			if content[off] == ')' || content[off] == ']' {

      
        398
        395
         				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}

      
        399
        
        -				emit(brSpan, SemOperator, modifierAbstract)

      
        
        396
        +				emit(brSpan, semOperator, modifierAbstract)

      
        400
        397
         				break

      
        401
        398
         			}

      
        402
        399
         		}

      
        403
        400
         	}

      
        404
        401
         

      
        405
        
        -	emit(p.Account.Span, SemanticAccount, 0)

      
        
        402
        +	emit(p.Account.Span, semAccount, 0)

      
        406
        403
         

      
        407
        404
         	if p.Amount != nil {

      
        408
        405
         		semEmitAmount(content, p.Amount, emit)

      ···
        414
        411
         		semEmitBalanceAssertion(content, p.Balance, emit)

      
        415
        412
         	}

      
        416
        413
         	if p.Comment != nil {

      
        417
        
        -		emit(p.Comment.Span, SemanticComment, 0)

      
        
        414
        +		emit(p.Comment.Span, semComment, 0)

      
        418
        415
         	}

      
        419
        416
         	for i := range p.Comments {

      
        420
        
        -		emit(p.Comments[i].Span, SemanticComment, 0)

      
        
        417
        +		emit(p.Comments[i].Span, semComment, 0)

      
        421
        418
         	}

      
        422
        419
         }

      
        423
        420
         

      ···
        442
        439
         	}

      
        443
        440
         	hasCommodity := a.CommoditySpan.Start.Offset > 0 && a.CommoditySpan.End.Offset > 0

      
        444
        441
         	if hasCommodity && a.CommodityPos == ast.CommodityBefore {

      
        445
        
        -		emit(a.CommoditySpan, SemanticCommodity, 0)

      
        
        442
        +		emit(a.CommoditySpan, semCommodity, 0)

      
        446
        443
         		semEmitQuantity(content, a, emit)

      
        447
        444
         		return

      
        448
        445
         	}

      
        449
        446
         	semEmitQuantity(content, a, emit)

      
        450
        447
         	if hasCommodity {

      
        451
        
        -		emit(a.CommoditySpan, SemanticCommodity, 0)

      
        
        448
        +		emit(a.CommoditySpan, semCommodity, 0)

      
        452
        449
         	}

      
        453
        450
         }

      
        454
        451
         

      ···
        461
        458
         	if a.IsNegative {

      
        462
        459
         		mods |= modifierNegative

      
        463
        460
         	}

      
        464
        
        -	emit(offsetSpan(a.Span.Start.File, qStart, qEnd), SemanticAmount, mods)

      
        
        461
        +	emit(offsetSpan(a.Span.Start.File, qStart, qEnd), semAmount, mods)

      
        465
        462
         }

      
        466
        463
         

      
        467
        464
         func quantitySpan(content string, a *ast.Amount) (int, int) {

      ···
        489
        486
         

      
        490
        487
         func emitDirective(content string, e token.Span, kwLen int, valType uint32, comment *ast.Comment, emit semEmitFn) {

      
        491
        488
         	kwEnd := e.Start.Offset + kwLen

      
        492
        
        -	emit(token.Span{Start: e.Start, End: offsetPos(e.Start.File, kwEnd)}, SemanticDirective, 0)

      
        
        489
        +	emit(token.Span{Start: e.Start, End: offsetPos(e.Start.File, kwEnd)}, semDirective, 0)

      
        493
        490
         	if v, ok := directiveValue(content, e, comment, kwEnd); ok {

      
        494
        491
         		emit(v, valType, 0)

      
        495
        492
         	}

      
        496
        493
         	if comment != nil {

      
        497
        
        -		emit(comment.Span, SemanticComment, 0)

      
        
        494
        +		emit(comment.Span, semComment, 0)

      
        498
        495
         	}

      
        499
        496
         }

      
        500
        497
         

      
        501
        498
         func semEmitCost(content string, c *ast.Cost, emit semEmitFn) {

      
        502
        499
         	if c.IsTotal {

      
        503
        
        -		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+2)}, SemOperator, 0)

      
        
        500
        +		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+2)}, semOperator, 0)

      
        504
        501
         	} else {

      
        505
        
        -		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+1)}, SemOperator, 0)

      
        
        502
        +		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+1)}, semOperator, 0)

      
        506
        503
         	}

      
        507
        504
         	semEmitAmount(content, &c.Amount, emit)

      
        508
        505
         }

      ···
        518
        515
         	for opEnd < ba.Span.End.Offset && (content[opEnd] == '=' || content[opEnd] == ':' || content[opEnd] == '*') {

      
        519
        516
         		opEnd++

      
        520
        517
         	}

      
        521
        
        -	emit(token.Span{Start: offsetPos(ba.Span.Start.File, opStart), End: offsetPos(ba.Span.Start.File, opEnd)}, SemOperator, 0)

      
        
        518
        +	emit(token.Span{Start: offsetPos(ba.Span.Start.File, opStart), End: offsetPos(ba.Span.Start.File, opEnd)}, semOperator, 0)

      
        522
        519
         	semEmitAmount(content, &ba.Amount, emit)

      
        523
        520
         	if ba.Cost != nil {

      
        524
        521
         		semEmitCost(content, ba.Cost, emit)

      ···
        537
        534
         		tok := l.Next()

      
        538
        535
         		if tok.Type == token.EOF {

      
        539
        536
         			if commentStart > 0 {

      
        540
        
        -				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, SemanticComment, 0)

      
        
        537
        +				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, semComment, 0)

      
        541
        538
         			}

      
        542
        539
         			break

      
        543
        540
         		}

      
        544
        541
         		if tok.Type == token.NEWLINE {

      
        545
        542
         			if commentStart > 0 {

      
        546
        
        -				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, SemanticComment, 0)

      
        
        543
        +				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, semComment, 0)

      
        547
        544
         				commentStart, commentEnd = 0, 0

      
        548
        545
         			}

      
        549
        546
         			continue

      ···
        551
        548
         		if tok.Type == token.WHITESPACE || tok.Type == token.INDENT {

      
        552
        549
         			continue

      
        553
        550
         		}

      
        554
        
        -		tokType := uint32(SemString)

      
        
        551
        +		tokType := uint32(semString)

      
        555
        552
         		if commentStart > 0 {

      
        556
        
        -			tokType = SemanticComment

      
        
        553
        +			tokType = semComment

      
        557
        554
         			if tok.Span.End.Offset > commentEnd {

      
        558
        555
         				commentEnd = tok.Span.End.Offset

      
        559
        556
         			}

      ···
        561
        558
         		}

      
        562
        559
         		switch tok.Type {

      
        563
        560
         		case token.SEMICOLON, token.HASH, token.PERCENT:

      
        564
        
        -			tokType = SemanticComment

      
        
        561
        +			tokType = semComment

      
        565
        562
         			commentStart = tok.Span.Start.Offset

      
        566
        563
         			commentEnd = tok.Span.End.Offset

      
        567
        564
         			continue

      
        568
        565
         		case token.STAR:

      
        569
        
        -			tokType = SemanticComment // * at col 0 is comment marker

      
        
        566
        +			tokType = semComment // * at col 0 is comment marker

      
        570
        567
         			commentStart = tok.Span.Start.Offset

      
        571
        568
         			commentEnd = tok.Span.End.Offset

      
        572
        569
         			continue

      
        573
        570
         		case token.ACCOUNT, token.COMMODITY, token.INCLUDE, token.ALIAS,

      
        574
        571
         			token.PAYEE, token.TAG, token.APPLY, token.END, token.COMMENTKW,

      
        575
        572
         			token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:

      
        576
        
        -			tokType = SemanticDirective

      
        
        573
        +			tokType = semDirective

      
        577
        574
         		case token.DATE:

      
        578
        
        -			tokType = SemanticDate

      
        
        575
        +			tokType = semDate

      
        579
        576
         		case token.INT, token.DECIMAL:

      
        580
        
        -			tokType = SemanticAmount

      
        
        577
        +			tokType = semAmount

      
        581
        578
         		case token.COMMODITYMARK:

      
        582
        
        -			tokType = SemanticCommodity

      
        
        579
        +			tokType = semCommodity

      
        583
        580
         		case token.BANG:

      
        584
        
        -			tokType = SemanticStatus

      
        
        581
        +			tokType = semStatus

      
        585
        582
         		case token.AT, token.ATAT, token.EQ, token.EQEQ, token.EQEQEQ, token.EQSTAR:

      
        586
        
        -			tokType = SemOperator

      
        
        583
        +			tokType = semOperator

      
        587
        584
         		}

      
        588
        585
         		take(tok.Span, tokType, 0)

      
        589
        586
         	}

      
M internal/lsp/textdocument_semantic_test.go
···
        1
        1
         package lsp

      
        2
        2
         

      
        3
        3
         import (

      
        4
        
        -	"context"

      
        5
        4
         	"fmt"

      
        6
        5
         	"slices"

      
        7
        6
         	"strings"

      ···
        13
        12
         	"olexsmir.xyz/clerk/internal/testutil/golden"

      
        14
        13
         )

      
        15
        14
         

      
        16
        
        -func TestSemanticTokens_Legend(t *testing.T) {

      
        17
        
        -	if len(tokenTypeStrings) != semTypeCount {

      
        18
        
        -		t.Fatalf("tokenTypeStrings has %d entries, want %d (one per TokenType constant)",

      
        19
        
        -			len(tokenTypeStrings), semTypeCount)

      
        20
        
        -	}

      
        21
        
        -	legend := getSemanticTokensLegend()

      
        22
        
        -	if !slices.Equal(legend.TokenTypes, tokenTypeStrings) {

      
        23
        
        -		t.Errorf("legend.TokenTypes = %v, want %v", legend.TokenTypes, tokenTypeStrings)

      
        24
        
        -	}

      
        25
        
        -	if !slices.Equal(legend.TokenModifiers, modifierStrings) {

      
        26
        
        -		t.Errorf("legend.TokenModifiers = %v, want %v", legend.TokenModifiers, modifierStrings)

      
        27
        
        -	}

      
        28
        
        -}

      
        29
        
        -

      
        30
        
        -func TestSemanticTokensEncode(t *testing.T) {

      
        31
        
        -	tests := []struct {

      
        32
        
        -		name   string

      
        
        15
        +func TestEncodeSemTokens(t *testing.T) {

      
        
        16
        +	tests := map[string]struct {

      
        33
        17
         		tokens []semanticToken

      
        34
        18
         		want   []uint32

      
        35
        19
         	}{

      
        36
        
        -		{"nil", nil, nil},

      
        37
        
        -		{"empty", []semanticToken{}, nil},

      
        38
        
        -		{"single", []semanticToken{

      
        39
        
        -			{line: 0, col: 0, length: 4, tokenType: SemanticDate},

      
        40
        
        -		}, []uint32{0, 0, 4, SemanticDate, 0}},

      
        41
        
        -		{"line 0 col 0", []semanticToken{

      
        42
        
        -			{line: 0, col: 0, length: 1, tokenType: SemanticDirective},

      
        43
        
        -		}, []uint32{0, 0, 1, SemanticDirective, 0}},

      
        44
        
        -		{"multiple", []semanticToken{

      
        45
        
        -			{line: 0, col: 0, length: 10, tokenType: SemanticDate},

      
        46
        
        -			{line: 0, col: 11, length: 5, tokenType: SemString},

      
        47
        
        -			{line: 1, col: 4, length: 10, tokenType: SemanticAccount},

      
        
        20
        +		"nil":   {nil, nil},

      
        
        21
        +		"empty": {[]semanticToken{}, nil},

      
        
        22
        +		"single": {[]semanticToken{

      
        
        23
        +			{line: 0, col: 0, length: 4, tokenType: semDate},

      
        
        24
        +		}, []uint32{0, 0, 4, semDate, 0}},

      
        
        25
        +		"line 0 col 0": {[]semanticToken{

      
        
        26
        +			{line: 0, col: 0, length: 1, tokenType: semDirective},

      
        
        27
        +		}, []uint32{0, 0, 1, semDirective, 0}},

      
        
        28
        +		"multiple": {[]semanticToken{

      
        
        29
        +			{line: 0, col: 0, length: 10, tokenType: semDate},

      
        
        30
        +			{line: 0, col: 11, length: 5, tokenType: semString},

      
        
        31
        +			{line: 1, col: 4, length: 10, tokenType: semAccount},

      
        48
        32
         		}, []uint32{

      
        49
        
        -			0, 0, 10, SemanticDate, 0,

      
        50
        
        -			0, 11, 5, SemString, 0,

      
        51
        
        -			1, 4, 10, SemanticAccount, 0,

      
        
        33
        +			0, 0, 10, semDate, 0,

      
        
        34
        +			0, 11, 5, semString, 0,

      
        
        35
        +			1, 4, 10, semAccount, 0,

      
        52
        36
         		}},

      
        53
        37
         		// the wire format requires non-negative deltas; unsorted input must

      
        54
        38
         		// be sorted first (commodity at col 36 comes after amount at col 32)

      
        55
        
        -		{"unsorted input", []semanticToken{

      
        56
        
        -			{line: 0, col: 0, length: 10, tokenType: SemanticDate},

      
        57
        
        -			{line: 0, col: 36, length: 3, tokenType: SemanticCommodity},

      
        58
        
        -			{line: 0, col: 32, length: 2, tokenType: SemanticAmount},

      
        59
        
        -			{line: 1, col: 4, length: 6, tokenType: SemanticAccount},

      
        
        39
        +		"unsorted input": {[]semanticToken{

      
        
        40
        +			{line: 0, col: 0, length: 10, tokenType: semDate},

      
        
        41
        +			{line: 0, col: 36, length: 3, tokenType: semCommodity},

      
        
        42
        +			{line: 0, col: 32, length: 2, tokenType: semAmount},

      
        
        43
        +			{line: 1, col: 4, length: 6, tokenType: semAccount},

      
        60
        44
         		}, []uint32{

      
        61
        
        -			0, 0, 10, SemanticDate, 0,

      
        62
        
        -			0, 32, 2, SemanticAmount, 0,

      
        63
        
        -			0, 4, 3, SemanticCommodity, 0,

      
        64
        
        -			1, 4, 6, SemanticAccount, 0,

      
        
        45
        +			0, 0, 10, semDate, 0,

      
        
        46
        +			0, 32, 2, semAmount, 0,

      
        
        47
        +			0, 4, 3, semCommodity, 0,

      
        
        48
        +			1, 4, 6, semAccount, 0,

      
        65
        49
         		}},

      
        66
        50
         	}

      
        67
        
        -	for _, tt := range tests {

      
        68
        
        -		t.Run(tt.name, func(t *testing.T) {

      
        
        51
        +

      
        
        52
        +	for tname, tt := range tests {

      
        
        53
        +		t.Run(tname, func(t *testing.T) {

      
        69
        54
         			if got := encodeSemTokens(tt.tokens); !slices.Equal(got, tt.want) {

      
        70
        55
         				t.Errorf("encodeSemTokens() = %v, want %v", got, tt.want)

      
        71
        56
         			}

      ···
        73
        58
         	}

      
        74
        59
         }

      
        75
        60
         

      
        76
        
        -func TestSemanticTokens_Server_SimpleTransaction(t *testing.T) {

      
        77
        
        -	content := "2024-01-15 test\n    expenses:food  $50\n    assets:cash\n"

      
        
        61
        +func TestServer_Semantic_SimpleTransaction(t *testing.T) {

      
        
        62
        +	content := `2024-01-15 test

      
        
        63
        +    expenses:food  $50

      
        
        64
        +    assets:cash

      
        
        65
        +`

      
        78
        66
         

      
        79
        67
         	srv := NewServer("test")

      
        80
        68
         	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")

      ···
        100
        88
         	}

      
        101
        89
         }

      
        102
        90
         

      
        103
        
        -func TestSemanticTokens_Server_EmptyDocument(t *testing.T) {

      
        
        91
        +func TestServer_Semantic_EmptyDocument(t *testing.T) {

      
        104
        92
         	srv := NewServer("test")

      
        105
        93
         	srv.server.openDoc(uri.URI("file:///empty.journal"), "", 1, "journal")

      
        106
        94
         

      ···
        115
        103
         	}

      
        116
        104
         }

      
        117
        105
         

      
        118
        
        -func TestSemanticTokens_Server_DocumentNotFound(t *testing.T) {

      
        
        106
        +func TestServer_Semantic_DocumentNotFound(t *testing.T) {

      
        119
        107
         	result, err := NewServer("test").server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        120
        108
         		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///unknown.journal")},

      
        121
        109
         	})

      ···
        130
        118
         	}

      
        131
        119
         }

      
        132
        120
         

      
        133
        
        -func TestSemanticTokens_Server_Range(t *testing.T) {

      
        134
        
        -	content := "2024-01-15 test\n    expenses:food  $50\n2024-01-16 other\n    expenses:drinks  $20\n"

      
        
        121
        +func TestServer_Semantic_Range(t *testing.T) {

      
        
        122
        +	content := `2024-01-15 test

      
        
        123
        +    expenses:food  $50

      
        
        124
        +

      
        
        125
        +2024-01-16 other

      
        
        126
        +  expenses:drinks  $20

      
        
        127
        +`

      
        
        128
        +

      
        135
        129
         	srv := NewServer("test")

      
        136
        130
         	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")

      
        137
        131
         

      
        138
        
        -	params := &protocol.SemanticTokensRangeParams{

      
        
        132
        +	result, err := srv.server.SemanticTokensRange(t.Context(), &protocol.SemanticTokensRangeParams{

      
        139
        133
         		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},

      
        140
        134
         		Range: protocol.Range{

      
        141
        135
         			Start: protocol.Position{Line: 0, Character: 0},

      
        142
        136
         			End:   protocol.Position{Line: 1, Character: 50},

      
        143
        137
         		},

      
        144
        
        -	}

      
        145
        
        -	result, err := srv.server.SemanticTokensRange(context.Background(), params)

      
        
        138
        +	})

      
        146
        139
         	if err != nil {

      
        147
        140
         		t.Fatal(err)

      
        148
        141
         	}

      ···
        179
        172
         		ar := golden.Read(t, tt)

      
        180
        173
         

      
        181
        174
         		t.Run(tt+"_golden", func(t *testing.T) {

      
        182
        
        -			toks := renderSemanticTokens(tokSem(string(ar.Get("in.journal"))))

      
        
        175
        +			toks := renderSemanticTokens(tokSem(ar.Get("in.journal")))

      
        183
        176
         			golden.Assert(t, ar, toks)

      
        184
        177
         		})

      
        185
        178
         

      
        186
        179
         		t.Run(tt+"_no-overlap", func(t *testing.T) {

      
        187
        
        -			toks := tokSem(string(ar.Get("in.journal")))

      
        
        180
        +			toks := tokSem(ar.Get("in.journal"))

      
        188
        181
         			slices.SortFunc(toks, func(a, b semanticToken) int {

      
        189
        182
         				if a.line != b.line {

      
        190
        183
         					return int(a.line) - int(b.line)

      ···
        228
        221
         	return b.String()

      
        229
        222
         }

      
        230
        223
         

      
        231
        
        -func tokSem(content string) []semanticToken {

      
        232
        
        -	return tokenizeForSemantics(content, parseJournalStr(content))

      
        
        224
        +func tokSem(content []byte) []semanticToken {

      
        
        225
        +	c := string(content)

      
        
        226
        +	return tokenizeForSemantics(c, parseJournalStr(c))

      
        233
        227
         }

      
M internal/lsp/textdocument_sync.go
···
        35
        35
         

      
        36
        36
         // Document management

      
        37
        37
         

      
        
        38
        +type docState struct {

      
        
        39
        +	text       string

      
        
        40
        +	version    int32

      
        
        41
        +	languageID protocol.LanguageKind

      
        
        42
        +	semTokens  []semanticToken // cached semantic tokens

      
        
        43
        +}

      
        
        44
        +

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

      
        39
        46
         	s.mu.Lock()

      
        40
        47
         	s.openDocs[u] = docState{

      ···
        65
        72
         	s.openDocs[u] = state

      
        66
        73
         }

      
        67
        74
         

      
        68
        
        -func (s *server) closeDoc(u uri.URI) {

      
        69
        
        -	s.mu.Lock()

      
        70
        
        -	delete(s.openDocs, u)

      
        71
        
        -	s.mu.Unlock()

      
        72
        
        -}

      
        73
        
        -

      
        74
        
        -func (s *server) getDocText(u uri.URI) (string, bool) {

      
        
        75
        +func (s *server) getDocState(u uri.URI) (docState, bool) {

      
        75
        76
         	s.mu.Lock()

      
        76
        77
         	state, ok := s.openDocs[u]

      
        77
        78
         	s.mu.Unlock()

      
        78
        
        -	return state.text, ok

      
        
        79
        +	return state, ok

      
        79
        80
         }

      
        80
        81
         

      
        81
        
        -func (s *server) getDocState(u uri.URI) (docState, bool) {

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

      
        82
        83
         	s.mu.Lock()

      
        83
        
        -	state, ok := s.openDocs[u]

      
        
        84
        +	delete(s.openDocs, u)

      
        84
        85
         	s.mu.Unlock()

      
        85
        
        -	return state, ok

      
        86
        86
         }

      
        87
        87
         

      
        88
        
        -// parseJournalStr parses a raw journal string into an AST.

      
        89
        88
         func parseJournalStr(content string) *ast.Journal {

      
        90
        89
         	l := lexer.New("", []byte(content))

      
        91
        90
         	p := parser.New(l)

      
M journal/journal.go
···
        5
        5
         	"strings"

      
        6
        6
         )

      
        7
        7
         

      
        8
        
        -func IsJournalFile(name string) bool {

      
        9
        
        -	ext := strings.ToLower(filepath.Ext(name))

      
        10
        
        -	return extensionSet[ext]

      
        11
        
        -}

      
        12
        
        -

      
        13
        8
         var (

      
        14
        9
         	extensionSet        map[string]bool

      
        15
        10
         	SupportedExtensions = [...]string{

      
        16
        
        -		".journal", ".hledger",

      
        
        11
        +		".journal", ".jrnl", ".j", ".hledger",

      
        17
        12
         		".dat", ".ledger",

      
        18
        
        -		".jrnl",

      
        19
        13
         	}

      
        20
        14
         )

      
        21
        15
         

      ···
        25
        19
         		extensionSet[ext] = true

      
        26
        20
         	}

      
        27
        21
         }

      
        
        22
        +

      
        
        23
        +func IsJournalFile(name string) bool {

      
        
        24
        +	ext := strings.ToLower(filepath.Ext(name))

      
        
        25
        +	return extensionSet[ext]

      
        
        26
        +}