10 files changed,
184 insertions(+),
201 deletions(-)
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
jump to
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 +}