hledger-lsp 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +87 -167
  2. package/out/features/codeActions.d.ts +0 -4
  3. package/out/features/codeActions.d.ts.map +1 -1
  4. package/out/features/codeActions.js +11 -25
  5. package/out/features/codeActions.js.map +1 -1
  6. package/out/features/codeLens.d.ts +29 -0
  7. package/out/features/codeLens.d.ts.map +1 -0
  8. package/out/features/codeLens.js +105 -0
  9. package/out/features/codeLens.js.map +1 -0
  10. package/out/features/completion.d.ts.map +1 -1
  11. package/out/features/completion.js +21 -9
  12. package/out/features/completion.js.map +1 -1
  13. package/out/features/definition.d.ts.map +1 -1
  14. package/out/features/definition.js.map +1 -1
  15. package/out/features/foldingRanges.d.ts.map +1 -1
  16. package/out/features/foldingRanges.js +9 -2
  17. package/out/features/foldingRanges.js.map +1 -1
  18. package/out/features/formatter.d.ts +2 -0
  19. package/out/features/formatter.d.ts.map +1 -1
  20. package/out/features/formatter.js +36 -7
  21. package/out/features/formatter.js.map +1 -1
  22. package/out/features/formattingUtils.d.ts +38 -0
  23. package/out/features/formattingUtils.d.ts.map +1 -0
  24. package/out/features/formattingUtils.js +107 -0
  25. package/out/features/formattingUtils.js.map +1 -0
  26. package/out/features/hover.d.ts +2 -5
  27. package/out/features/hover.d.ts.map +1 -1
  28. package/out/features/hover.js +60 -36
  29. package/out/features/hover.js.map +1 -1
  30. package/out/features/inlayHints.d.ts +7 -5
  31. package/out/features/inlayHints.d.ts.map +1 -1
  32. package/out/features/inlayHints.js +164 -91
  33. package/out/features/inlayHints.js.map +1 -1
  34. package/out/features/semanticTokens.d.ts.map +1 -1
  35. package/out/features/semanticTokens.js +22 -1
  36. package/out/features/semanticTokens.js.map +1 -1
  37. package/out/features/symbols.d.ts.map +1 -1
  38. package/out/features/symbols.js +9 -3
  39. package/out/features/symbols.js.map +1 -1
  40. package/out/features/validator.d.ts +0 -8
  41. package/out/features/validator.d.ts.map +1 -1
  42. package/out/features/validator.js +367 -171
  43. package/out/features/validator.js.map +1 -1
  44. package/out/parser/ast.d.ts +2 -2
  45. package/out/parser/ast.d.ts.map +1 -1
  46. package/out/parser/ast.js +10 -5
  47. package/out/parser/ast.js.map +1 -1
  48. package/out/parser/index.d.ts +8 -5
  49. package/out/parser/index.d.ts.map +1 -1
  50. package/out/parser/index.js +0 -7
  51. package/out/parser/index.js.map +1 -1
  52. package/out/server/configFile.d.ts +104 -0
  53. package/out/server/configFile.d.ts.map +1 -0
  54. package/out/server/configFile.js +231 -0
  55. package/out/server/configFile.js.map +1 -0
  56. package/out/server/settings.d.ts +9 -0
  57. package/out/server/settings.d.ts.map +1 -1
  58. package/out/server/settings.js +9 -0
  59. package/out/server/settings.js.map +1 -1
  60. package/out/server/workspace.d.ts +126 -0
  61. package/out/server/workspace.d.ts.map +1 -0
  62. package/out/server/workspace.js +605 -0
  63. package/out/server/workspace.js.map +1 -0
  64. package/out/server.js +438 -29
  65. package/out/server.js.map +1 -1
  66. package/out/types.d.ts +15 -4
  67. package/out/types.d.ts.map +1 -1
  68. package/out/types.js +11 -0
  69. package/out/types.js.map +1 -1
  70. package/out/utils/amountFormatter.d.ts +26 -0
  71. package/out/utils/amountFormatter.d.ts.map +1 -0
  72. package/out/utils/amountFormatter.js +91 -0
  73. package/out/utils/amountFormatter.js.map +1 -0
  74. package/out/utils/balanceCalculator.d.ts +32 -0
  75. package/out/utils/balanceCalculator.d.ts.map +1 -0
  76. package/out/utils/balanceCalculator.js +93 -0
  77. package/out/utils/balanceCalculator.js.map +1 -0
  78. package/out/utils/index.d.ts.map +1 -1
  79. package/out/utils/index.js +2 -1
  80. package/out/utils/index.js.map +1 -1
  81. package/package.json +1 -1
@@ -13,6 +13,8 @@ exports.validator = exports.Validator = void 0;
13
13
  const node_1 = require("vscode-languageserver/node");
14
14
  const uri_1 = require("../utils/uri");
15
15
  const settings_1 = require("../server/settings");
16
+ const balanceCalculator_1 = require("../utils/balanceCalculator");
17
+ const amountFormatter_1 = require("../utils/amountFormatter");
16
18
  class Validator {
17
19
  /**
18
20
  * Validate a parsed hledger document
@@ -30,11 +32,18 @@ class Validator {
30
32
  // Otherwise use defaults
31
33
  return settings_1.defaultSettings.validation?.[key] ?? true;
32
34
  };
35
+ // Normalize document URI for consistent comparison
36
+ const normalizedDocUri = (0, uri_1.toFileUri)((0, uri_1.toFilePath)(document.uri));
33
37
  // Validate each transaction
34
38
  for (const transaction of parsedDoc.transactions) {
39
+ // Only validate transactions in the current document
40
+ // (workspace parsing may include transactions from other files)
41
+ if (transaction.sourceUri !== normalizedDocUri) {
42
+ continue;
43
+ }
35
44
  // Check balance
36
45
  if (isEnabled('balance')) {
37
- const balanceIssues = this.validateBalance(transaction, document);
46
+ const balanceIssues = this.validateBalance(transaction, document, parsedDoc);
38
47
  diagnostics.push(...balanceIssues);
39
48
  }
40
49
  // Check missing amounts
@@ -87,36 +96,15 @@ class Validator {
87
96
  * Validate transaction balance
88
97
  * Transactions must balance (all amounts sum to zero per commodity)
89
98
  */
90
- validateBalance(transaction, document) {
99
+ validateBalance(transaction, document, parsedDoc) {
91
100
  const diagnostics = [];
92
- // Group postings by commodity
93
- const balances = new Map();
101
+ // Calculate transaction balance by commodity
102
+ const balances = (0, balanceCalculator_1.calculateTransactionBalance)(transaction);
94
103
  // Count how many postings have amounts
95
104
  let postingsWithAmounts = 0;
96
105
  for (const posting of transaction.postings) {
97
106
  if (posting.amount) {
98
107
  postingsWithAmounts++;
99
- // If posting has a cost, use the cost commodity for balance calculation
100
- if (posting.cost) {
101
- const costCommodity = posting.cost.amount.commodity || '';
102
- let costValue;
103
- if (posting.cost.type === 'unit') {
104
- // @ unitPrice: total cost = quantity * unitPrice
105
- costValue = posting.amount.quantity * posting.cost.amount.quantity;
106
- }
107
- else {
108
- // @@ totalPrice: use total price directly
109
- costValue = posting.cost.amount.quantity;
110
- }
111
- const current = balances.get(costCommodity) || 0;
112
- balances.set(costCommodity, current + costValue);
113
- }
114
- else {
115
- // No cost notation, use the posting's commodity
116
- const commodity = posting.amount.commodity || '';
117
- const current = balances.get(commodity) || 0;
118
- balances.set(commodity, current + posting.amount.quantity);
119
- }
120
108
  }
121
109
  }
122
110
  // If all postings have amounts, check if they balance
@@ -124,11 +112,13 @@ class Validator {
124
112
  for (const [commodity, balance] of balances.entries()) {
125
113
  // Allow for small floating point errors
126
114
  if (Math.abs(balance) > 0.005) {
127
- const commodityStr = commodity ? ` ${commodity}` : '';
115
+ const formattedBalance = commodity
116
+ ? (0, amountFormatter_1.formatAmount)(balance, commodity, parsedDoc)
117
+ : balance.toFixed(2);
128
118
  diagnostics.push({
129
119
  severity: node_1.DiagnosticSeverity.Error,
130
120
  range: this.getTransactionRange(transaction, document),
131
- message: `Transaction does not balance: ${balance.toFixed(2)}${commodityStr} off`,
121
+ message: `Transaction does not balance: ${formattedBalance} off`,
132
122
  source: 'hledger'
133
123
  });
134
124
  }
@@ -159,6 +149,8 @@ class Validator {
159
149
  */
160
150
  validateUndeclaredItems(document, parsedDoc, settings, checkAccounts = true, checkPayees = true, checkCommodities = true, checkTags = true) {
161
151
  const diagnostics = [];
152
+ // Normalize document URI for consistent comparison
153
+ const normalizedDocUri = (0, uri_1.toFileUri)((0, uri_1.toFilePath)(document.uri));
162
154
  // Helper to convert severity string to DiagnosticSeverity
163
155
  const getSeverity = (severityStr) => {
164
156
  switch (severityStr) {
@@ -173,77 +165,281 @@ class Validator {
173
165
  const markAllInstances = settings?.validation?.markAllUndeclaredInstances ?? settings_1.defaultSettings.validation?.markAllUndeclaredInstances ?? true;
174
166
  // Check undeclared accounts (if enabled)
175
167
  if (checkAccounts) {
176
- const undeclaredAccounts = Array.from(parsedDoc.accounts.values()).filter(a => !a.declared);
177
- for (const account of undeclaredAccounts) {
178
- const ranges = markAllInstances
179
- ? this.findAllOccurrences(document, account.name)
180
- : (() => { const r = this.findFirstOccurrence(document, account.name); return r ? [r] : []; })();
181
- for (const range of ranges) {
182
- diagnostics.push({
183
- severity: getSeverity(settings?.severity?.undeclaredAccounts),
184
- range,
185
- message: `Account "${account.name}" is used but not declared with 'account' directive`,
186
- source: 'hledger',
187
- code: 'undeclared-account',
188
- data: { accountName: account.name }
189
- });
168
+ const undeclaredAccounts = new Set(Array.from(parsedDoc.accounts.values()).filter(a => !a.declared).map(a => a.name));
169
+ if (undeclaredAccounts.size > 0) {
170
+ const text = document.getText();
171
+ const lines = text.split('\n');
172
+ const processedAccounts = new Set();
173
+ // Iterate through transactions to find account usage locations
174
+ for (const transaction of parsedDoc.transactions) {
175
+ // Only process transactions from the current document
176
+ if (transaction.sourceUri && transaction.sourceUri !== normalizedDocUri) {
177
+ continue;
178
+ }
179
+ if (transaction.line !== undefined) {
180
+ let postingLineOffset = 1;
181
+ for (const posting of transaction.postings) {
182
+ const accountName = posting.account;
183
+ if (undeclaredAccounts.has(accountName)) {
184
+ if (!markAllInstances && processedAccounts.has(accountName)) {
185
+ postingLineOffset++;
186
+ continue;
187
+ }
188
+ // Find the posting line
189
+ const postingLine = transaction.line + postingLineOffset;
190
+ if (postingLine < lines.length) {
191
+ const line = lines[postingLine];
192
+ // Account name is after indentation, before amount/comment
193
+ const accountIndex = line.indexOf(accountName);
194
+ if (accountIndex !== -1) {
195
+ diagnostics.push({
196
+ severity: getSeverity(settings?.severity?.undeclaredAccounts),
197
+ range: {
198
+ start: { line: postingLine, character: accountIndex },
199
+ end: { line: postingLine, character: accountIndex + accountName.length }
200
+ },
201
+ message: `Account "${accountName}" is used but not declared with 'account' directive`,
202
+ source: 'hledger',
203
+ code: 'undeclared-account',
204
+ data: { accountName }
205
+ });
206
+ processedAccounts.add(accountName);
207
+ }
208
+ }
209
+ }
210
+ postingLineOffset++;
211
+ }
212
+ }
190
213
  }
191
214
  }
192
215
  }
193
216
  // Check undeclared payees (if enabled)
194
217
  if (checkPayees) {
195
- const undeclaredPayees = Array.from(parsedDoc.payees.values()).filter(p => !p.declared);
196
- for (const payee of undeclaredPayees) {
197
- const ranges = markAllInstances
198
- ? this.findAllOccurrences(document, payee.name)
199
- : (() => { const r = this.findFirstOccurrence(document, payee.name); return r ? [r] : []; })();
200
- for (const range of ranges) {
201
- diagnostics.push({
202
- severity: getSeverity(settings?.severity?.undeclaredPayees),
203
- range,
204
- message: `Payee "${payee.name}" is used but not declared with 'payee' directive`,
205
- source: 'hledger',
206
- code: 'undeclared-payee',
207
- data: { payeeName: payee.name }
208
- });
218
+ const undeclaredPayees = new Set(Array.from(parsedDoc.payees.values()).filter(p => !p.declared).map(p => p.name));
219
+ if (undeclaredPayees.size > 0) {
220
+ const text = document.getText();
221
+ const lines = text.split('\n');
222
+ const processedPayees = new Set();
223
+ // Iterate through transactions to find payee usage locations
224
+ for (const transaction of parsedDoc.transactions) {
225
+ // Only process transactions from the current document
226
+ if (transaction.sourceUri && transaction.sourceUri !== normalizedDocUri) {
227
+ continue;
228
+ }
229
+ const payeeName = transaction.payee;
230
+ if (undeclaredPayees.has(payeeName) && transaction.line !== undefined) {
231
+ if (!markAllInstances && processedPayees.has(payeeName)) {
232
+ continue;
233
+ }
234
+ const line = lines[transaction.line];
235
+ // Payee is in the transaction header after date, status, and code
236
+ const payeeIndex = line.indexOf(payeeName);
237
+ if (payeeIndex !== -1) {
238
+ diagnostics.push({
239
+ severity: getSeverity(settings?.severity?.undeclaredPayees),
240
+ range: {
241
+ start: { line: transaction.line, character: payeeIndex },
242
+ end: { line: transaction.line, character: payeeIndex + payeeName.length }
243
+ },
244
+ message: `Payee "${payeeName}" is used but not declared with 'payee' directive`,
245
+ source: 'hledger',
246
+ code: 'undeclared-payee',
247
+ data: { payeeName }
248
+ });
249
+ processedPayees.add(payeeName);
250
+ }
251
+ }
209
252
  }
210
253
  }
211
254
  }
212
255
  // Check undeclared commodities (if enabled)
213
256
  if (checkCommodities) {
214
- const undeclaredCommodities = Array.from(parsedDoc.commodities.values()).filter(c => !c.declared);
215
- for (const commodity of undeclaredCommodities) {
216
- const ranges = markAllInstances
217
- ? this.findAllOccurrences(document, commodity.name)
218
- : (() => { const r = this.findFirstOccurrence(document, commodity.name); return r ? [r] : []; })();
219
- for (const range of ranges) {
220
- diagnostics.push({
221
- severity: getSeverity(settings?.severity?.undeclaredCommodities),
222
- range,
223
- message: `Commodity "${commodity.name}" is used but not declared with 'commodity' directive`,
224
- source: 'hledger',
225
- code: 'undeclared-commodity',
226
- data: { commodityName: commodity.name }
227
- });
257
+ const undeclaredCommodities = new Set(Array.from(parsedDoc.commodities.values()).filter(c => !c.declared).map(c => c.name));
258
+ if (undeclaredCommodities.size > 0) {
259
+ const text = document.getText();
260
+ const lines = text.split('\n');
261
+ const processedCommodities = new Set();
262
+ // Iterate through transactions to find commodity usage locations
263
+ for (const transaction of parsedDoc.transactions) {
264
+ // Only process transactions from the current document
265
+ if (transaction.sourceUri && transaction.sourceUri !== normalizedDocUri) {
266
+ continue;
267
+ }
268
+ if (transaction.line !== undefined) {
269
+ let postingLineOffset = 1;
270
+ for (const posting of transaction.postings) {
271
+ const postingLine = transaction.line + postingLineOffset;
272
+ if (postingLine < lines.length) {
273
+ const line = lines[postingLine];
274
+ // Check commodity in posting amount
275
+ if (posting.amount?.commodity && undeclaredCommodities.has(posting.amount.commodity)) {
276
+ const commodityName = posting.amount.commodity;
277
+ if (!markAllInstances && processedCommodities.has(commodityName)) {
278
+ postingLineOffset++;
279
+ continue;
280
+ }
281
+ const commodityIndex = line.indexOf(commodityName);
282
+ if (commodityIndex !== -1) {
283
+ diagnostics.push({
284
+ severity: getSeverity(settings?.severity?.undeclaredCommodities),
285
+ range: {
286
+ start: { line: postingLine, character: commodityIndex },
287
+ end: { line: postingLine, character: commodityIndex + commodityName.length }
288
+ },
289
+ message: `Commodity "${commodityName}" is used but not declared with 'commodity' directive`,
290
+ source: 'hledger',
291
+ code: 'undeclared-commodity',
292
+ data: { commodityName }
293
+ });
294
+ processedCommodities.add(commodityName);
295
+ }
296
+ }
297
+ // Check commodity in cost notation
298
+ if (posting.cost?.amount?.commodity && undeclaredCommodities.has(posting.cost.amount.commodity)) {
299
+ const commodityName = posting.cost.amount.commodity;
300
+ if (!markAllInstances && processedCommodities.has(commodityName)) {
301
+ postingLineOffset++;
302
+ continue;
303
+ }
304
+ // Cost appears after @ or @@
305
+ const atIndex = line.indexOf('@');
306
+ if (atIndex !== -1) {
307
+ const afterAt = line.substring(atIndex);
308
+ const commodityIndex = afterAt.indexOf(commodityName);
309
+ if (commodityIndex !== -1) {
310
+ const absoluteIndex = atIndex + commodityIndex;
311
+ diagnostics.push({
312
+ severity: getSeverity(settings?.severity?.undeclaredCommodities),
313
+ range: {
314
+ start: { line: postingLine, character: absoluteIndex },
315
+ end: { line: postingLine, character: absoluteIndex + commodityName.length }
316
+ },
317
+ message: `Commodity "${commodityName}" is used but not declared with 'commodity' directive`,
318
+ source: 'hledger',
319
+ code: 'undeclared-commodity',
320
+ data: { commodityName }
321
+ });
322
+ processedCommodities.add(commodityName);
323
+ }
324
+ }
325
+ }
326
+ // Check commodity in balance assertion
327
+ if (posting.assertion?.commodity && undeclaredCommodities.has(posting.assertion.commodity)) {
328
+ const commodityName = posting.assertion.commodity;
329
+ if (!markAllInstances && processedCommodities.has(commodityName)) {
330
+ postingLineOffset++;
331
+ continue;
332
+ }
333
+ // Assertion appears after =
334
+ const equalsIndex = line.indexOf('=');
335
+ if (equalsIndex !== -1) {
336
+ const afterEquals = line.substring(equalsIndex);
337
+ const commodityIndex = afterEquals.indexOf(commodityName);
338
+ if (commodityIndex !== -1) {
339
+ const absoluteIndex = equalsIndex + commodityIndex;
340
+ diagnostics.push({
341
+ severity: getSeverity(settings?.severity?.undeclaredCommodities),
342
+ range: {
343
+ start: { line: postingLine, character: absoluteIndex },
344
+ end: { line: postingLine, character: absoluteIndex + commodityName.length }
345
+ },
346
+ message: `Commodity "${commodityName}" is used but not declared with 'commodity' directive`,
347
+ source: 'hledger',
348
+ code: 'undeclared-commodity',
349
+ data: { commodityName }
350
+ });
351
+ processedCommodities.add(commodityName);
352
+ }
353
+ }
354
+ }
355
+ }
356
+ postingLineOffset++;
357
+ }
358
+ }
228
359
  }
229
360
  }
230
361
  }
231
362
  // Check undeclared tags (if enabled)
232
363
  if (checkTags) {
233
- const undeclaredTags = Array.from(parsedDoc.tags.values()).filter(t => !t.declared);
234
- for (const tag of undeclaredTags) {
235
- const ranges = markAllInstances
236
- ? this.findAllOccurrences(document, tag.name + ':')
237
- : (() => { const r = this.findFirstOccurrence(document, tag.name + ':'); return r ? [r] : []; })();
238
- for (const range of ranges) {
239
- diagnostics.push({
240
- severity: getSeverity(settings?.severity?.undeclaredTags || 'information'),
241
- range,
242
- message: `Tag "${tag.name}" is used but not declared with 'tag' directive`,
243
- source: 'hledger',
244
- code: 'undeclared-tag',
245
- data: { tagName: tag.name }
246
- });
364
+ const undeclaredTags = new Set(Array.from(parsedDoc.tags.values()).filter(t => !t.declared).map(t => t.name));
365
+ if (undeclaredTags.size > 0) {
366
+ const text = document.getText();
367
+ const lines = text.split('\n');
368
+ const processedTags = new Set();
369
+ // Iterate through transactions to find tag usage locations
370
+ for (const transaction of parsedDoc.transactions) {
371
+ // Only process transactions from the current document
372
+ if (transaction.sourceUri && transaction.sourceUri !== normalizedDocUri) {
373
+ continue;
374
+ }
375
+ // Check transaction-level tags
376
+ if (transaction.tags) {
377
+ for (const tagName of Object.keys(transaction.tags)) {
378
+ if (undeclaredTags.has(tagName) && transaction.line !== undefined) {
379
+ if (!markAllInstances && processedTags.has(tagName)) {
380
+ continue; // Skip if we already reported this tag
381
+ }
382
+ const line = lines[transaction.line];
383
+ const commentIndex = line.indexOf(';');
384
+ if (commentIndex !== -1) {
385
+ const tagIndex = line.indexOf(tagName + ':', commentIndex);
386
+ if (tagIndex !== -1) {
387
+ diagnostics.push({
388
+ severity: getSeverity(settings?.severity?.undeclaredTags || 'information'),
389
+ range: {
390
+ start: { line: transaction.line, character: tagIndex },
391
+ end: { line: transaction.line, character: tagIndex + tagName.length + 1 }
392
+ },
393
+ message: `Tag "${tagName}" is used but not declared with 'tag' directive`,
394
+ source: 'hledger',
395
+ code: 'undeclared-tag',
396
+ data: { tagName }
397
+ });
398
+ processedTags.add(tagName);
399
+ }
400
+ }
401
+ }
402
+ }
403
+ }
404
+ // Check posting-level tags
405
+ if (transaction.line !== undefined) {
406
+ let postingLineOffset = 1;
407
+ for (const posting of transaction.postings) {
408
+ if (posting.tags) {
409
+ for (const tagName of Object.keys(posting.tags)) {
410
+ if (undeclaredTags.has(tagName)) {
411
+ if (!markAllInstances && processedTags.has(tagName)) {
412
+ continue;
413
+ }
414
+ // Find the posting line
415
+ const postingLine = transaction.line + postingLineOffset;
416
+ if (postingLine < lines.length) {
417
+ const line = lines[postingLine];
418
+ const commentIndex = line.indexOf(';');
419
+ if (commentIndex !== -1) {
420
+ const tagIndex = line.indexOf(tagName + ':', commentIndex);
421
+ if (tagIndex !== -1) {
422
+ diagnostics.push({
423
+ severity: getSeverity(settings?.severity?.undeclaredTags || 'information'),
424
+ range: {
425
+ start: { line: postingLine, character: tagIndex },
426
+ end: { line: postingLine, character: tagIndex + tagName.length + 1 }
427
+ },
428
+ message: `Tag "${tagName}" is used but not declared with 'tag' directive`,
429
+ source: 'hledger',
430
+ code: 'undeclared-tag',
431
+ data: { tagName }
432
+ });
433
+ processedTags.add(tagName);
434
+ }
435
+ }
436
+ }
437
+ }
438
+ }
439
+ }
440
+ postingLineOffset++;
441
+ }
442
+ }
247
443
  }
248
444
  }
249
445
  }
@@ -289,74 +485,30 @@ class Validator {
289
485
  }
290
486
  return null;
291
487
  }
292
- /**
293
- * Find all occurrences of a string in the document
294
- */
295
- findAllOccurrences(document, searchStr) {
296
- const text = document.getText();
297
- const lines = text.split('\n');
298
- const ranges = [];
299
- for (let i = 0; i < lines.length; i++) {
300
- const line = lines[i];
301
- let startIndex = 0;
302
- let index;
303
- // Find all occurrences in this line
304
- while ((index = line.indexOf(searchStr, startIndex)) !== -1) {
305
- ranges.push({
306
- start: { line: i, character: index },
307
- end: { line: i, character: index + searchStr.length }
308
- });
309
- startIndex = index + 1; // Move past this occurrence to find next
310
- }
311
- }
312
- return ranges;
313
- }
314
488
  /**
315
489
  * Validate date ordering
316
490
  * Warn if transactions are not in chronological order
317
491
  */
318
492
  validateDateOrdering(transactions, document) {
319
493
  const diagnostics = [];
320
- for (let i = 1; i < transactions.length; i++) {
321
- const prevDate = this.parseDate(transactions[i - 1].date);
322
- const currDate = this.parseDate(transactions[i].date);
494
+ // Only validate transactions in the current document
495
+ const normalizedDocUri = (0, uri_1.toFileUri)((0, uri_1.toFilePath)(document.uri));
496
+ const documentTransactions = transactions.filter(t => t.sourceUri === normalizedDocUri);
497
+ for (let i = 1; i < documentTransactions.length; i++) {
498
+ const prevDate = this.parseDate(documentTransactions[i - 1].date);
499
+ const currDate = this.parseDate(documentTransactions[i].date);
323
500
  if (prevDate && currDate && currDate < prevDate) {
324
- const range = this.getTransactionRange(transactions[i], document);
501
+ const range = this.getTransactionRange(documentTransactions[i], document);
325
502
  diagnostics.push({
326
503
  severity: node_1.DiagnosticSeverity.Warning,
327
504
  range,
328
- message: `Transaction date ${transactions[i].date} is before previous transaction date ${transactions[i - 1].date}`,
505
+ message: `Transaction date ${documentTransactions[i].date} is before previous transaction date ${documentTransactions[i - 1].date}`,
329
506
  source: 'hledger'
330
507
  });
331
508
  }
332
509
  }
333
510
  return diagnostics;
334
511
  }
335
- /**
336
- * Format an amount with commodity according to declared format
337
- */
338
- formatAmountWithCommodity(quantity, commodityName, parsedDoc) {
339
- // Find commodity format
340
- const commodity = parsedDoc.commodities.get(commodityName);
341
- if (!commodity?.format) {
342
- // No format declared, use default: amount then commodity with space
343
- return commodityName ? `${quantity.toFixed(2)} ${commodityName}` : quantity.toFixed(2);
344
- }
345
- const format = commodity.format;
346
- const symbol = format.symbol || commodityName;
347
- const symbolOnLeft = format.symbolOnLeft ?? false;
348
- const spaceBetween = format.spaceBetween ?? true;
349
- const space = spaceBetween ? ' ' : '';
350
- // Use precision from format, or default to 2
351
- const precision = format.precision ?? 2;
352
- const formattedNumber = quantity.toFixed(precision);
353
- if (symbolOnLeft) {
354
- return `${symbol}${space}${formattedNumber}`;
355
- }
356
- else {
357
- return `${formattedNumber}${space}${symbol}`;
358
- }
359
- }
360
512
  /**
361
513
  * Validate balance assertions
362
514
  * Check if balance assertions match calculated balances
@@ -365,6 +517,8 @@ class Validator {
365
517
  const diagnostics = [];
366
518
  // Track running balances per account per commodity
367
519
  const balances = new Map();
520
+ // Process ALL transactions to calculate running balances correctly
521
+ // (workspace parsing includes transactions from all files)
368
522
  for (const transaction of transactions) {
369
523
  for (const posting of transaction.postings) {
370
524
  // Update running balance
@@ -379,8 +533,9 @@ class Validator {
379
533
  accountBalances.set(commodity, currentBalance + posting.amount.quantity);
380
534
  balances.set(posting.account, accountBalances);
381
535
  }
382
- // Check assertion
383
- if (posting.assertion) {
536
+ // Check assertion - but only create diagnostics for assertions in the current document
537
+ const normalizedDocUri = (0, uri_1.toFileUri)((0, uri_1.toFilePath)(document.uri));
538
+ if (posting.assertion && transaction.sourceUri === normalizedDocUri) {
384
539
  const accountBalances = balances.get(posting.account);
385
540
  const commodity = posting.assertion.commodity || '';
386
541
  const expectedBalance = posting.assertion.quantity;
@@ -388,8 +543,8 @@ class Validator {
388
543
  // Allow for small floating point errors
389
544
  if (Math.abs(actualBalance - expectedBalance) > 0.005) {
390
545
  const range = this.findPostingRange(transaction, posting, document);
391
- const expectedFormatted = this.formatAmountWithCommodity(expectedBalance, commodity, parsedDoc);
392
- const actualFormatted = this.formatAmountWithCommodity(actualBalance, commodity, parsedDoc);
546
+ const expectedFormatted = (0, amountFormatter_1.formatAmount)(expectedBalance, commodity, parsedDoc);
547
+ const actualFormatted = (0, amountFormatter_1.formatAmount)(actualBalance, commodity, parsedDoc);
393
548
  diagnostics.push({
394
549
  severity: node_1.DiagnosticSeverity.Error,
395
550
  range,
@@ -580,50 +735,91 @@ class Validator {
580
735
  const includeDirectives = parsedDoc.directives.filter(d => d.type === 'include');
581
736
  for (const directive of includeDirectives) {
582
737
  const includePath = directive.value;
583
- // Resolve the include path
584
- const resolvedPath = (0, uri_1.resolveIncludePath)(includePath, baseUri);
585
- // Check for duplicate includes in the same document
586
- if (visited.has(resolvedPath)) {
587
- const range = this.findFirstOccurrence(document, includePath);
588
- if (range) {
589
- diagnostics.push({
590
- severity: node_1.DiagnosticSeverity.Warning,
591
- range,
592
- message: `Duplicate include: ${includePath}`,
593
- source: 'hledger'
594
- });
738
+ // Check if this is a glob pattern
739
+ const isGlob = /[*?\[\]{}]/.test(includePath);
740
+ if (isGlob) {
741
+ // For glob patterns, expand to all matching files
742
+ const resolvedPaths = (0, uri_1.resolveIncludePaths)(includePath, baseUri);
743
+ // Check if glob matched any files (if enabled)
744
+ if (resolvedPaths.length === 0 && checkMissingFiles) {
745
+ const range = this.findFirstOccurrence(document, includePath);
746
+ if (range) {
747
+ diagnostics.push({
748
+ severity: node_1.DiagnosticSeverity.Error,
749
+ range,
750
+ message: `Include glob pattern matches no files: ${includePath}`,
751
+ source: 'hledger'
752
+ });
753
+ }
595
754
  }
596
- continue;
597
- }
598
- visited.add(resolvedPath);
599
- // Check if file exists (if enabled)
600
- const includeDoc = fileReader(resolvedPath);
601
- if (!includeDoc && checkMissingFiles) {
602
- // File doesn't exist
603
- const range = this.findFirstOccurrence(document, includePath);
604
- if (range) {
605
- diagnostics.push({
606
- severity: node_1.DiagnosticSeverity.Error,
607
- range,
608
- message: `Include file not found: ${includePath}`,
609
- source: 'hledger'
610
- });
755
+ // Check for circular includes for each matched file
756
+ if (checkCircularIncludes) {
757
+ for (const resolvedPath of resolvedPaths) {
758
+ const includeDoc = fileReader(resolvedPath);
759
+ if (includeDoc) {
760
+ const circularCheck = this.checkCircularInclude(document.uri, includeDoc, fileReader, new Set([baseUri, resolvedPath]));
761
+ if (circularCheck) {
762
+ const range = this.findFirstOccurrence(document, includePath);
763
+ if (range) {
764
+ diagnostics.push({
765
+ severity: node_1.DiagnosticSeverity.Error,
766
+ range,
767
+ message: `Circular include detected in glob: ${includePath} (via ${resolvedPath})`,
768
+ source: 'hledger'
769
+ });
770
+ break; // Only report once per glob pattern
771
+ }
772
+ }
773
+ }
774
+ }
611
775
  }
612
776
  }
613
- // Check for circular includes (if enabled and file exists)
614
- if (includeDoc && checkCircularIncludes) {
615
- const circularCheck = this.checkCircularInclude(document.uri, includeDoc, fileReader, new Set([baseUri, resolvedPath]));
616
- if (circularCheck) {
777
+ else {
778
+ // Single file include (existing logic)
779
+ const resolvedPath = (0, uri_1.resolveIncludePath)(includePath, baseUri);
780
+ // Check for duplicate includes in the same document
781
+ if (visited.has(resolvedPath)) {
782
+ const range = this.findFirstOccurrence(document, includePath);
783
+ if (range) {
784
+ diagnostics.push({
785
+ severity: node_1.DiagnosticSeverity.Warning,
786
+ range,
787
+ message: `Duplicate include: ${includePath}`,
788
+ source: 'hledger'
789
+ });
790
+ }
791
+ continue;
792
+ }
793
+ visited.add(resolvedPath);
794
+ // Check if file exists (if enabled)
795
+ const includeDoc = fileReader(resolvedPath);
796
+ if (!includeDoc && checkMissingFiles) {
797
+ // File doesn't exist
617
798
  const range = this.findFirstOccurrence(document, includePath);
618
799
  if (range) {
619
800
  diagnostics.push({
620
801
  severity: node_1.DiagnosticSeverity.Error,
621
802
  range,
622
- message: `Circular include detected: ${includePath}`,
803
+ message: `Include file not found: ${includePath}`,
623
804
  source: 'hledger'
624
805
  });
625
806
  }
626
807
  }
808
+ // Check for circular includes (if enabled and file exists)
809
+ if (includeDoc && checkCircularIncludes) {
810
+ const circularCheck = this.checkCircularInclude(document.uri, includeDoc, fileReader, new Set([baseUri, resolvedPath]));
811
+ if (circularCheck) {
812
+ const range = this.findFirstOccurrence(document, includePath);
813
+ if (range) {
814
+ diagnostics.push({
815
+ severity: node_1.DiagnosticSeverity.Error,
816
+ range,
817
+ message: `Circular include detected: ${includePath}`,
818
+ source: 'hledger'
819
+ });
820
+ }
821
+ }
822
+ }
627
823
  }
628
824
  }
629
825
  return diagnostics;