hledger-lsp 0.1.0

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 (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +632 -0
  3. package/out/extension.d.ts +1 -0
  4. package/out/extension.d.ts.map +1 -0
  5. package/out/extension.js +2 -0
  6. package/out/extension.js.map +1 -0
  7. package/out/features/codeActions.d.ts +73 -0
  8. package/out/features/codeActions.d.ts.map +1 -0
  9. package/out/features/codeActions.js +417 -0
  10. package/out/features/codeActions.js.map +1 -0
  11. package/out/features/completion.d.ts +94 -0
  12. package/out/features/completion.d.ts.map +1 -0
  13. package/out/features/completion.js +323 -0
  14. package/out/features/completion.js.map +1 -0
  15. package/out/features/definition.d.ts +12 -0
  16. package/out/features/definition.d.ts.map +1 -0
  17. package/out/features/definition.js +61 -0
  18. package/out/features/definition.js.map +1 -0
  19. package/out/features/documentLinks.d.ts +17 -0
  20. package/out/features/documentLinks.d.ts.map +1 -0
  21. package/out/features/documentLinks.js +68 -0
  22. package/out/features/documentLinks.js.map +1 -0
  23. package/out/features/findReferences.d.ts +33 -0
  24. package/out/features/findReferences.d.ts.map +1 -0
  25. package/out/features/findReferences.js +79 -0
  26. package/out/features/findReferences.js.map +1 -0
  27. package/out/features/foldingRanges.d.ts +27 -0
  28. package/out/features/foldingRanges.d.ts.map +1 -0
  29. package/out/features/foldingRanges.js +111 -0
  30. package/out/features/foldingRanges.js.map +1 -0
  31. package/out/features/formatter.d.ts +70 -0
  32. package/out/features/formatter.d.ts.map +1 -0
  33. package/out/features/formatter.js +373 -0
  34. package/out/features/formatter.js.map +1 -0
  35. package/out/features/hover.d.ts +66 -0
  36. package/out/features/hover.d.ts.map +1 -0
  37. package/out/features/hover.js +387 -0
  38. package/out/features/hover.js.map +1 -0
  39. package/out/features/inlayHints.d.ts +43 -0
  40. package/out/features/inlayHints.d.ts.map +1 -0
  41. package/out/features/inlayHints.js +221 -0
  42. package/out/features/inlayHints.js.map +1 -0
  43. package/out/features/selectionRange.d.ts +47 -0
  44. package/out/features/selectionRange.d.ts.map +1 -0
  45. package/out/features/selectionRange.js +273 -0
  46. package/out/features/selectionRange.js.map +1 -0
  47. package/out/features/semanticTokens.d.ts +83 -0
  48. package/out/features/semanticTokens.d.ts.map +1 -0
  49. package/out/features/semanticTokens.js +370 -0
  50. package/out/features/semanticTokens.js.map +1 -0
  51. package/out/features/symbols.d.ts +47 -0
  52. package/out/features/symbols.d.ts.map +1 -0
  53. package/out/features/symbols.js +249 -0
  54. package/out/features/symbols.js.map +1 -0
  55. package/out/features/transactionAnalyzer.d.ts +63 -0
  56. package/out/features/transactionAnalyzer.d.ts.map +1 -0
  57. package/out/features/transactionAnalyzer.js +127 -0
  58. package/out/features/transactionAnalyzer.js.map +1 -0
  59. package/out/features/validator.d.ts +142 -0
  60. package/out/features/validator.d.ts.map +1 -0
  61. package/out/features/validator.js +633 -0
  62. package/out/features/validator.js.map +1 -0
  63. package/out/parser/ast.d.ts +37 -0
  64. package/out/parser/ast.d.ts.map +1 -0
  65. package/out/parser/ast.js +606 -0
  66. package/out/parser/ast.js.map +1 -0
  67. package/out/parser/includes.d.ts +25 -0
  68. package/out/parser/includes.d.ts.map +1 -0
  69. package/out/parser/includes.js +106 -0
  70. package/out/parser/includes.js.map +1 -0
  71. package/out/parser/index.d.ts +54 -0
  72. package/out/parser/index.d.ts.map +1 -0
  73. package/out/parser/index.js +146 -0
  74. package/out/parser/index.js.map +1 -0
  75. package/out/server/deps.d.ts +19 -0
  76. package/out/server/deps.d.ts.map +1 -0
  77. package/out/server/deps.js +77 -0
  78. package/out/server/deps.js.map +1 -0
  79. package/out/server/settings.d.ts +60 -0
  80. package/out/server/settings.d.ts.map +1 -0
  81. package/out/server/settings.js +110 -0
  82. package/out/server/settings.js.map +1 -0
  83. package/out/server.d.ts +3 -0
  84. package/out/server.d.ts.map +1 -0
  85. package/out/server.js +420 -0
  86. package/out/server.js.map +1 -0
  87. package/out/types.d.ts +84 -0
  88. package/out/types.d.ts.map +1 -0
  89. package/out/types.js +6 -0
  90. package/out/types.js.map +1 -0
  91. package/out/utils/index.d.ts +38 -0
  92. package/out/utils/index.d.ts.map +1 -0
  93. package/out/utils/index.js +89 -0
  94. package/out/utils/index.js.map +1 -0
  95. package/out/utils/uri.d.ts +32 -0
  96. package/out/utils/uri.d.ts.map +1 -0
  97. package/out/utils/uri.js +215 -0
  98. package/out/utils/uri.js.map +1 -0
  99. package/package.json +58 -0
@@ -0,0 +1,37 @@
1
+ import { Transaction, Posting, Amount, Account, Payee, Commodity, Tag, Directive } from '../types';
2
+ import { TextDocument } from 'vscode-languageserver-textdocument';
3
+ /**
4
+ * Parse a transaction starting at startLine within lines array.
5
+ * This is a pure helper extracted from HledgerParser.
6
+ */
7
+ export declare function parseTransaction(lines: string[], startLine: number): Transaction | null;
8
+ /**
9
+ * Infer costs for two-commodity transactions without explicit cost notation.
10
+ *
11
+ * According to hledger docs, when a transaction has:
12
+ * - All postings with amounts specified
13
+ * - Exactly two commodities
14
+ * - No explicit cost notation
15
+ *
16
+ * Then hledger infers a total cost on the first posting to balance the transaction.
17
+ */
18
+ export declare function inferCosts(transaction: Transaction): void;
19
+ export declare function parseTransactionHeader(line: string): {
20
+ date: string;
21
+ effectiveDate?: string;
22
+ status?: 'cleared' | 'pending' | 'unmarked';
23
+ code?: string;
24
+ description: string;
25
+ payee: string;
26
+ note: string;
27
+ comment?: string;
28
+ tags?: Record<string, string>;
29
+ } | null;
30
+ export declare function parsePosting(line: string): Posting | null;
31
+ export declare function parseAmount(amountStr: string): Amount | null;
32
+ export declare function extractAccounts(document: TextDocument, sourceUri?: string): Account[];
33
+ export declare function extractPayees(document: TextDocument, sourceUri?: string): Payee[];
34
+ export declare function extractCommodities(document: TextDocument, sourceUri?: string): Commodity[];
35
+ export declare function extractTagNames(document: TextDocument, sourceUri?: string): Tag[];
36
+ export declare function parseDirective(line: string): Directive | null;
37
+ //# sourceMappingURL=ast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ast.d.ts","sourceRoot":"","sources":["../../src/parser/ast.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAQ,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACzG,OAAO,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAGlE;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA8DvF;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,WAAW,GAAG,IAAI,CAkDzD;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAiDnQ;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAsEzD;AAED,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAqB5D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,EAAE,CAwBrF;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,CAyBjF;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,EAAE,CA0G1F;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,EAAE,CA4CjF;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAyB7D"}
@@ -0,0 +1,606 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseTransaction = parseTransaction;
4
+ exports.inferCosts = inferCosts;
5
+ exports.parseTransactionHeader = parseTransactionHeader;
6
+ exports.parsePosting = parsePosting;
7
+ exports.parseAmount = parseAmount;
8
+ exports.extractAccounts = extractAccounts;
9
+ exports.extractPayees = extractPayees;
10
+ exports.extractCommodities = extractCommodities;
11
+ exports.extractTagNames = extractTagNames;
12
+ exports.parseDirective = parseDirective;
13
+ const index_1 = require("../utils/index");
14
+ /**
15
+ * Parse a transaction starting at startLine within lines array.
16
+ * This is a pure helper extracted from HledgerParser.
17
+ */
18
+ function parseTransaction(lines, startLine) {
19
+ if (!lines || lines.length === 0 || startLine >= lines.length) {
20
+ return null;
21
+ }
22
+ const headerLine = lines[startLine];
23
+ if (!(0, index_1.isTransactionHeader)(headerLine))
24
+ return null;
25
+ // reuse the header parser below
26
+ const header = parseTransactionHeader(headerLine);
27
+ if (!header)
28
+ return null;
29
+ const postings = [];
30
+ let currentLine = startLine + 1;
31
+ let transactionComment;
32
+ const transactionTags = {};
33
+ while (currentLine < lines.length) {
34
+ const line = lines[currentLine];
35
+ if ((0, index_1.isTransactionHeader)(line) || (!line.trim().startsWith(';') && !line.trim().startsWith('#') && line.trim() && !(0, index_1.isPosting)(line))) {
36
+ break;
37
+ }
38
+ if ((0, index_1.isComment)(line)) {
39
+ const commentText = line.trim().substring(1).trim();
40
+ if (!transactionComment)
41
+ transactionComment = commentText;
42
+ const tags = (0, index_1.extractTags)(commentText);
43
+ Object.assign(transactionTags, tags);
44
+ currentLine++;
45
+ continue;
46
+ }
47
+ if ((0, index_1.isPosting)(line)) {
48
+ const posting = parsePosting(line);
49
+ if (posting)
50
+ postings.push(posting);
51
+ }
52
+ currentLine++;
53
+ if (!line.trim())
54
+ break;
55
+ }
56
+ const transaction = {
57
+ date: header.date,
58
+ description: header.description,
59
+ payee: header.payee,
60
+ note: header.note,
61
+ postings,
62
+ line: startLine
63
+ };
64
+ if (header.effectiveDate)
65
+ transaction.effectiveDate = header.effectiveDate;
66
+ if (header.status)
67
+ transaction.status = header.status;
68
+ if (header.code)
69
+ transaction.code = header.code;
70
+ if (header.comment || transactionComment)
71
+ transaction.comment = header.comment || transactionComment;
72
+ if (Object.keys(transactionTags).length > 0 || header.tags)
73
+ transaction.tags = { ...transactionTags, ...header.tags };
74
+ // Infer costs for two-commodity transactions
75
+ inferCosts(transaction);
76
+ return transaction;
77
+ }
78
+ /**
79
+ * Infer costs for two-commodity transactions without explicit cost notation.
80
+ *
81
+ * According to hledger docs, when a transaction has:
82
+ * - All postings with amounts specified
83
+ * - Exactly two commodities
84
+ * - No explicit cost notation
85
+ *
86
+ * Then hledger infers a total cost on the first posting to balance the transaction.
87
+ */
88
+ function inferCosts(transaction) {
89
+ // Requirements for cost inference
90
+ if (transaction.postings.length === 0)
91
+ return;
92
+ // 1. All postings must have amounts
93
+ const allHaveAmounts = transaction.postings.every(p => p.amount);
94
+ if (!allHaveAmounts)
95
+ return;
96
+ // 2. No posting should already have explicit cost notation
97
+ const hasExplicitCost = transaction.postings.some(p => p.cost);
98
+ if (hasExplicitCost)
99
+ return;
100
+ // 3. Count distinct commodities
101
+ const commodities = new Set();
102
+ for (const posting of transaction.postings) {
103
+ if (posting.amount) {
104
+ commodities.add(posting.amount.commodity || '');
105
+ }
106
+ }
107
+ // Must have exactly 2 commodities
108
+ if (commodities.size !== 2)
109
+ return;
110
+ // Get first posting's commodity
111
+ const firstCommodity = transaction.postings[0].amount.commodity || '';
112
+ // Sum all amounts in the other commodity
113
+ let otherCommodity = '';
114
+ let otherSum = 0;
115
+ for (const posting of transaction.postings) {
116
+ const commodity = posting.amount.commodity || '';
117
+ if (commodity !== firstCommodity) {
118
+ otherCommodity = commodity;
119
+ otherSum += posting.amount.quantity;
120
+ }
121
+ }
122
+ // Infer total cost: the negation of the sum of other commodity
123
+ // This makes the transaction balance when cost is used for balance calculation
124
+ const costAmount = {
125
+ quantity: -otherSum,
126
+ commodity: otherCommodity
127
+ };
128
+ // Add inferred total cost to first posting
129
+ transaction.postings[0].cost = {
130
+ type: 'total',
131
+ amount: costAmount
132
+ };
133
+ }
134
+ function parseTransactionHeader(line) {
135
+ const trimmed = line.trim();
136
+ const dateMatch = trimmed.match(/^(\d{4}[-/]\d{2}[-/]\d{2})/);
137
+ if (!dateMatch)
138
+ return null;
139
+ const date = dateMatch[1];
140
+ let rest = trimmed.substring(date.length).trim();
141
+ let effectiveDate;
142
+ const effectiveDateMatch = rest.match(/^=(\d{4}[-/]\d{2}[-/]\d{2})/);
143
+ if (effectiveDateMatch) {
144
+ effectiveDate = effectiveDateMatch[1];
145
+ rest = rest.substring(effectiveDateMatch[0].length).trim();
146
+ }
147
+ let status;
148
+ if (rest.startsWith('*')) {
149
+ status = 'cleared';
150
+ rest = rest.substring(1).trim();
151
+ }
152
+ else if (rest.startsWith('!')) {
153
+ status = 'pending';
154
+ rest = rest.substring(1).trim();
155
+ }
156
+ let code;
157
+ const codeMatch = rest.match(/^\(([^)]+)\)/);
158
+ if (codeMatch) {
159
+ code = codeMatch[1];
160
+ rest = rest.substring(codeMatch[0].length).trim();
161
+ }
162
+ let comment;
163
+ let tags;
164
+ const commentMatch = rest.match(/^([^;]*);(.*)$/);
165
+ if (commentMatch) {
166
+ rest = commentMatch[1].trim();
167
+ comment = commentMatch[2].trim();
168
+ const extracted = (0, index_1.extractTags)(comment);
169
+ if (Object.keys(extracted).length > 0)
170
+ tags = extracted;
171
+ }
172
+ const description = rest.trim();
173
+ // Parse payee and note according to hledger spec:
174
+ // If description contains |, split into payee (left) and note (right)
175
+ // If no |, payee and note both equal description
176
+ let payee;
177
+ let note;
178
+ const pipeIndex = description.indexOf('|');
179
+ if (pipeIndex !== -1) {
180
+ payee = description.substring(0, pipeIndex).trim();
181
+ note = description.substring(pipeIndex + 1).trim();
182
+ }
183
+ else {
184
+ payee = description;
185
+ note = description;
186
+ }
187
+ return { date, effectiveDate, status, code, description, payee, note, comment, tags };
188
+ }
189
+ function parsePosting(line) {
190
+ if (!(0, index_1.isPosting)(line))
191
+ return null;
192
+ const account = (0, index_1.extractAccountFromPosting)(line);
193
+ if (!account)
194
+ return null;
195
+ const posting = { account };
196
+ const commentMatch = line.match(/^([^;]*);(.*)$/);
197
+ let mainPart = line;
198
+ let commentPart = '';
199
+ if (commentMatch) {
200
+ mainPart = commentMatch[1];
201
+ commentPart = commentMatch[2];
202
+ posting.comment = commentPart.trim();
203
+ const tags = (0, index_1.extractTags)(commentPart);
204
+ if (Object.keys(tags).length > 0)
205
+ posting.tags = tags;
206
+ }
207
+ const trimmed = mainPart.trim();
208
+ const afterAccount = trimmed.substring(account.length).trim();
209
+ if (!afterAccount)
210
+ return posting;
211
+ // Parse order: amount [@ cost | @@ cost] [= assertion]
212
+ // First, split on assertion (=)
213
+ const assertionMatch = afterAccount.match(/=\s*(.+)$/);
214
+ const beforeAssertion = assertionMatch
215
+ ? afterAccount.substring(0, assertionMatch.index ?? 0).trim()
216
+ : afterAccount;
217
+ if (assertionMatch) {
218
+ const assertionPart = assertionMatch[1].trim();
219
+ const assertionAmount = parseAmount(assertionPart);
220
+ if (assertionAmount)
221
+ posting.assertion = assertionAmount;
222
+ }
223
+ // Now parse amount and cost from beforeAssertion
224
+ // Check for @@ first (total price), then @ (unit price)
225
+ const totalCostMatch = beforeAssertion.match(/@@\s*(.+)$/);
226
+ const unitCostMatch = !totalCostMatch ? beforeAssertion.match(/@\s*(.+)$/) : null;
227
+ if (totalCostMatch) {
228
+ // Parse total cost: amount @@ totalCost
229
+ const amountPart = beforeAssertion.substring(0, totalCostMatch.index ?? 0).trim();
230
+ const costPart = totalCostMatch[1].trim();
231
+ const amount = parseAmount(amountPart);
232
+ if (amount)
233
+ posting.amount = amount;
234
+ const costAmount = parseAmount(costPart);
235
+ if (costAmount) {
236
+ posting.cost = { type: 'total', amount: costAmount };
237
+ }
238
+ }
239
+ else if (unitCostMatch) {
240
+ // Parse unit cost: amount @ unitCost
241
+ const amountPart = beforeAssertion.substring(0, unitCostMatch.index ?? 0).trim();
242
+ const costPart = unitCostMatch[1].trim();
243
+ const amount = parseAmount(amountPart);
244
+ if (amount)
245
+ posting.amount = amount;
246
+ const costAmount = parseAmount(costPart);
247
+ if (costAmount) {
248
+ posting.cost = { type: 'unit', amount: costAmount };
249
+ }
250
+ }
251
+ else {
252
+ // No cost notation, just parse amount
253
+ const amount = parseAmount(beforeAssertion);
254
+ if (amount)
255
+ posting.amount = amount;
256
+ }
257
+ return posting;
258
+ }
259
+ function parseAmount(amountStr) {
260
+ const trimmed = amountStr.trim();
261
+ if (!trimmed)
262
+ return null;
263
+ const patterns = [
264
+ { pattern: /^-([^\d\s-]+)\s*(\d+(?:[,]\d{3})*(?:\.\d+)?)$/, handler: (m) => ({ quantity: -parseFloat(m[2].replace(/,/g, '')), commodity: m[1] }) },
265
+ { pattern: /^([^\d\s-]+)\s*([-]?\d+(?:[,]\d{3})*(?:\.\d+)?)$/, handler: (m) => ({ quantity: parseFloat(m[2].replace(/,/g, '')), commodity: m[1] }) },
266
+ { pattern: /^([-]?\d+(?:[,]\d{3})*(?:\.\d+)?)\s*([^\d\s]+)$/, handler: (m) => ({ quantity: parseFloat(m[1].replace(/,/g, '')), commodity: m[2] }) },
267
+ { pattern: /^([-]?\d+(?:[,]\d{3})*(?:\.\d+)?)$/, handler: (m) => ({ quantity: parseFloat(m[1].replace(/,/g, '')), commodity: '' }) }
268
+ ];
269
+ for (const { pattern, handler } of patterns) {
270
+ const match = trimmed.match(pattern);
271
+ if (match) {
272
+ const res = handler(match);
273
+ if (isNaN(res.quantity))
274
+ return null;
275
+ return { quantity: res.quantity, commodity: res.commodity };
276
+ }
277
+ }
278
+ return null;
279
+ }
280
+ function extractAccounts(document, sourceUri) {
281
+ const text = document.getText();
282
+ const lines = text.split('\n');
283
+ const accountMap = new Map();
284
+ for (let i = 0; i < lines.length; i++) {
285
+ const line = lines[i];
286
+ if (line.trim().startsWith('account ')) {
287
+ const accountName = line.trim().substring(8).split(';')[0].trim();
288
+ if (accountName) {
289
+ const acc = { name: accountName, declared: true };
290
+ if (sourceUri !== undefined) {
291
+ acc.sourceUri = sourceUri;
292
+ acc.line = i;
293
+ }
294
+ accountMap.set(accountName, acc);
295
+ }
296
+ }
297
+ if ((0, index_1.isPosting)(line)) {
298
+ const account = (0, index_1.extractAccountFromPosting)(line);
299
+ if (account && !accountMap.has(account)) {
300
+ const acc = { name: account, declared: false };
301
+ if (sourceUri !== undefined) {
302
+ acc.sourceUri = sourceUri;
303
+ acc.line = i;
304
+ }
305
+ accountMap.set(account, acc);
306
+ }
307
+ }
308
+ }
309
+ return Array.from(accountMap.values()).sort((a, b) => a.name.localeCompare(b.name));
310
+ }
311
+ function extractPayees(document, sourceUri) {
312
+ const text = document.getText();
313
+ const lines = text.split('\n');
314
+ const payeeMap = new Map();
315
+ for (let i = 0; i < lines.length; i++) {
316
+ const line = lines[i];
317
+ if (line.trim().startsWith('payee ')) {
318
+ const payeeName = line.trim().substring(6).split(';')[0].trim();
319
+ if (payeeName) {
320
+ const p = { name: payeeName, declared: true };
321
+ if (sourceUri !== undefined) {
322
+ p.sourceUri = sourceUri;
323
+ p.line = i;
324
+ }
325
+ payeeMap.set(payeeName, p);
326
+ }
327
+ }
328
+ if ((0, index_1.isTransactionHeader)(line)) {
329
+ const header = parseTransactionHeader(line);
330
+ // Use payee field instead of description (handles | splitting)
331
+ if (header && header.payee && !payeeMap.has(header.payee)) {
332
+ const p = { name: header.payee, declared: false };
333
+ if (sourceUri !== undefined) {
334
+ p.sourceUri = sourceUri;
335
+ p.line = i;
336
+ }
337
+ payeeMap.set(header.payee, p);
338
+ }
339
+ }
340
+ }
341
+ return Array.from(payeeMap.values()).sort((a, b) => a.name.localeCompare(b.name));
342
+ }
343
+ function extractCommodities(document, sourceUri) {
344
+ const text = document.getText();
345
+ const lines = text.split('\n');
346
+ const commodityMap = new Map();
347
+ const stripQuotes = (s) => { const t = s.trim(); if (t.length >= 2 && t.startsWith('"') && t.endsWith('"'))
348
+ return t.substring(1, t.length - 1); return t; };
349
+ const parseCommoditySample = (sample) => {
350
+ if (!sample)
351
+ return null;
352
+ const s = sample.trim();
353
+ const firstDigit = s.search(/\d/);
354
+ if (firstDigit === -1)
355
+ return null;
356
+ const allowed = /[0-9.,\u00A0\s]/;
357
+ let start = firstDigit;
358
+ let end = start;
359
+ while (end < s.length && allowed.test(s[end]))
360
+ end++;
361
+ const leftRaw = s.substring(0, start).trim();
362
+ const numberRaw = s.substring(start, end).trim();
363
+ const rightRaw = s.substring(end).trim();
364
+ if (!numberRaw)
365
+ return null;
366
+ const lastDot = numberRaw.lastIndexOf('.');
367
+ const lastComma = numberRaw.lastIndexOf(',');
368
+ let decimalMark;
369
+ let decimalIndex = -1;
370
+ if (lastDot === -1 && lastComma === -1)
371
+ decimalMark = undefined;
372
+ else if (lastDot > lastComma) {
373
+ decimalMark = '.';
374
+ decimalIndex = lastDot;
375
+ }
376
+ else {
377
+ decimalMark = ',';
378
+ decimalIndex = lastComma;
379
+ }
380
+ const integerPart = decimalIndex >= 0 ? numberRaw.substring(0, decimalIndex) : numberRaw;
381
+ const fractionalPart = decimalIndex >= 0 ? numberRaw.substring(decimalIndex + 1) : '';
382
+ const precision = decimalIndex >= 0 ? (fractionalPart.length > 0 ? fractionalPart.length : 0) : null;
383
+ const sepCounts = {};
384
+ for (let i = 0; i < integerPart.length; i++) {
385
+ const ch = integerPart[i];
386
+ if (ch < '0' || ch > '9')
387
+ sepCounts[ch] = (sepCounts[ch] || 0) + 1;
388
+ }
389
+ let thousandsSeparator = null;
390
+ const separators = Object.keys(sepCounts);
391
+ if (separators.length === 1)
392
+ thousandsSeparator = separators[0];
393
+ else if (separators.length > 1) {
394
+ let max = 0;
395
+ let pick = null;
396
+ for (const k of separators) {
397
+ if (sepCounts[k] > max) {
398
+ max = sepCounts[k];
399
+ pick = k;
400
+ }
401
+ }
402
+ thousandsSeparator = pick;
403
+ }
404
+ let rawSymbol = leftRaw || rightRaw || '';
405
+ rawSymbol = stripQuotes(rawSymbol);
406
+ const symbolOnLeft = Boolean(leftRaw);
407
+ let spaceBetween = false;
408
+ if (symbolOnLeft) {
409
+ const between = s.substring(0, firstDigit);
410
+ spaceBetween = /\s/.test(between.replace(stripQuotes(leftRaw), '')) || /\s/.test(leftRaw.slice(-1));
411
+ }
412
+ else {
413
+ const after = s.substring(end);
414
+ spaceBetween = /\s/.test(after.replace(stripQuotes(rightRaw), '')) || /\s/.test(s[end - 1]);
415
+ }
416
+ const format = { symbol: rawSymbol, symbolOnLeft, spaceBetween, decimalMark: decimalMark, thousandsSeparator: thousandsSeparator || null, precision };
417
+ let name = rawSymbol;
418
+ if (rightRaw) {
419
+ const candidate = stripQuotes(rightRaw);
420
+ if (/^[A-Za-z][A-Za-z0-9 _-]*$/.test(candidate) || (rightRaw.trim().startsWith('"') && rightRaw.trim().endsWith('"')))
421
+ name = candidate;
422
+ }
423
+ if (rawSymbol === '""')
424
+ name = '';
425
+ return { name, format };
426
+ };
427
+ for (let i = 0; i < lines.length; i++) {
428
+ const line = lines[i];
429
+ if (line.trim().startsWith('commodity ')) {
430
+ const directive = line.trim().substring(10).split(';')[0].trim();
431
+ if (!directive)
432
+ continue;
433
+ let parsed = null;
434
+ if (/\d/.test(directive))
435
+ parsed = parseCommoditySample(directive);
436
+ let commodityName;
437
+ let format;
438
+ if (parsed) {
439
+ commodityName = parsed.name;
440
+ format = parsed.format;
441
+ }
442
+ else {
443
+ commodityName = stripQuotes(directive);
444
+ format = undefined;
445
+ }
446
+ let look = i + 1;
447
+ while (look < lines.length) {
448
+ const next = lines[look];
449
+ if (!next.trim()) {
450
+ look++;
451
+ continue;
452
+ }
453
+ if (!/^\s+/.test(next))
454
+ break;
455
+ const trimmedNext = next.trim();
456
+ if (trimmedNext.startsWith('format ')) {
457
+ const rest = trimmedNext.substring(7).trim();
458
+ const m = rest.match(/^(".*?"|\S+)\s+(.*)$/);
459
+ if (m) {
460
+ const formatSymbolRaw = m[1];
461
+ const samplePart = m[2];
462
+ const parsedFormat = parseCommoditySample(samplePart) || parseCommoditySample(`${samplePart} ${formatSymbolRaw}`);
463
+ if (parsedFormat && parsedFormat.format) {
464
+ format = parsedFormat.format;
465
+ const fs = stripQuotes(formatSymbolRaw);
466
+ if (fs && fs !== '' && (!commodityName || commodityName === '' || commodityName === fs))
467
+ commodityName = fs;
468
+ }
469
+ }
470
+ }
471
+ look++;
472
+ }
473
+ const key = commodityName;
474
+ if (commodityMap.has(key)) {
475
+ const existing = commodityMap.get(key);
476
+ const merged = { ...existing, declared: existing.declared || true, format: existing.format || format };
477
+ if (sourceUri !== undefined) {
478
+ merged.sourceUri = existing.sourceUri || sourceUri;
479
+ merged.line = existing.line ?? i;
480
+ }
481
+ commodityMap.set(key, merged);
482
+ }
483
+ else {
484
+ const c = { name: commodityName, declared: true, format };
485
+ if (sourceUri !== undefined) {
486
+ c.sourceUri = sourceUri;
487
+ c.line = i;
488
+ }
489
+ commodityMap.set(key, c);
490
+ }
491
+ }
492
+ if ((0, index_1.isPosting)(line)) {
493
+ const posting = parsePosting(line);
494
+ if (posting?.amount?.commodity && posting.amount.commodity !== '') {
495
+ const key = posting.amount.commodity;
496
+ if (!commodityMap.has(key)) {
497
+ const c = { name: key, declared: false };
498
+ if (sourceUri !== undefined) {
499
+ c.sourceUri = sourceUri;
500
+ }
501
+ commodityMap.set(key, c);
502
+ }
503
+ }
504
+ // Also extract commodity from cost notation
505
+ if (posting?.cost?.amount?.commodity && posting.cost.amount.commodity !== '') {
506
+ const key = posting.cost.amount.commodity;
507
+ if (!commodityMap.has(key)) {
508
+ const c = { name: key, declared: false };
509
+ if (sourceUri !== undefined) {
510
+ c.sourceUri = sourceUri;
511
+ }
512
+ commodityMap.set(key, c);
513
+ }
514
+ }
515
+ }
516
+ }
517
+ return Array.from(commodityMap.values()).sort((a, b) => a.name.localeCompare(b.name));
518
+ }
519
+ function extractTagNames(document, sourceUri) {
520
+ const text = document.getText();
521
+ const lines = text.split('\n');
522
+ const tagMap = new Map();
523
+ for (let i = 0; i < lines.length; i++) {
524
+ const line = lines[i];
525
+ if (line.trim().startsWith('tag ')) {
526
+ const tagName = line.trim().substring(4).split(';')[0].trim();
527
+ if (tagName) {
528
+ const t = { name: tagName, declared: true };
529
+ if (sourceUri !== undefined) {
530
+ t.sourceUri = sourceUri;
531
+ t.line = i;
532
+ }
533
+ tagMap.set(tagName, t);
534
+ }
535
+ }
536
+ // Standalone or indented comment lines (transaction-level comments)
537
+ if ((0, index_1.isComment)(line)) {
538
+ const commentText = line.trim().substring(1);
539
+ const extracted = (0, index_1.extractTags)(commentText);
540
+ for (const k of Object.keys(extracted)) {
541
+ if (!tagMap.has(k)) {
542
+ const t = { name: k, declared: false };
543
+ if (sourceUri !== undefined) {
544
+ t.sourceUri = sourceUri;
545
+ t.line = i;
546
+ }
547
+ tagMap.set(k, t);
548
+ }
549
+ }
550
+ }
551
+ if ((0, index_1.isPosting)(line)) {
552
+ // Use parsePosting to extract tags from the posting's comment part only
553
+ const posting = parsePosting(line);
554
+ if (posting?.tags) {
555
+ for (const k of Object.keys(posting.tags)) {
556
+ if (!tagMap.has(k)) {
557
+ const t = { name: k, declared: false };
558
+ if (sourceUri !== undefined) {
559
+ t.sourceUri = sourceUri;
560
+ }
561
+ tagMap.set(k, t);
562
+ }
563
+ }
564
+ }
565
+ }
566
+ if ((0, index_1.isTransactionHeader)(line)) {
567
+ const m = line.match(/;(.+)$/);
568
+ if (m) {
569
+ const extracted = (0, index_1.extractTags)(m[1]);
570
+ for (const k of Object.keys(extracted)) {
571
+ if (!tagMap.has(k)) {
572
+ const t = { name: k, declared: false };
573
+ if (sourceUri !== undefined) {
574
+ t.sourceUri = sourceUri;
575
+ }
576
+ tagMap.set(k, t);
577
+ }
578
+ }
579
+ }
580
+ }
581
+ }
582
+ return Array.from(tagMap.values()).sort((a, b) => a.name.localeCompare(b.name));
583
+ }
584
+ function parseDirective(line) {
585
+ const trimmed = line.trim();
586
+ // Extract comment if present
587
+ const commentMatch = trimmed.match(/^([^;]*);(.*)$/);
588
+ const mainPart = commentMatch ? commentMatch[1].trim() : trimmed;
589
+ const comment = commentMatch ? commentMatch[2].trim() : undefined;
590
+ // Parse directive type and value
591
+ const directives = ['account', 'commodity', 'payee', 'tag', 'include', 'alias'];
592
+ for (const directiveType of directives) {
593
+ if (mainPart.startsWith(directiveType + ' ')) {
594
+ const value = mainPart.substring(directiveType.length + 1).trim();
595
+ if (value) {
596
+ return {
597
+ type: directiveType,
598
+ value,
599
+ comment
600
+ };
601
+ }
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ //# sourceMappingURL=ast.js.map