@markup-carve/carve-lsp 0.1.0 → 0.1.2

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 (67) hide show
  1. package/README.md +55 -0
  2. package/dist/analyze.d.ts +17 -1
  3. package/dist/analyze.js +57 -1
  4. package/dist/analyze.js.map +1 -1
  5. package/dist/analyze.test.js +95 -0
  6. package/dist/analyze.test.js.map +1 -1
  7. package/dist/definition.js +26 -2
  8. package/dist/definition.js.map +1 -1
  9. package/dist/definition.test.js +8 -0
  10. package/dist/definition.test.js.map +1 -1
  11. package/dist/folding.js +4 -4
  12. package/dist/folding.js.map +1 -1
  13. package/dist/hover.js +33 -16
  14. package/dist/hover.js.map +1 -1
  15. package/dist/include-directive.d.ts +61 -0
  16. package/dist/include-directive.js +158 -0
  17. package/dist/include-directive.js.map +1 -0
  18. package/dist/include-directive.test.d.ts +1 -0
  19. package/dist/include-directive.test.js +83 -0
  20. package/dist/include-directive.test.js.map +1 -0
  21. package/dist/include-path.d.ts +63 -0
  22. package/dist/include-path.js +135 -0
  23. package/dist/include-path.js.map +1 -0
  24. package/dist/include-path.test.d.ts +1 -0
  25. package/dist/include-path.test.js +189 -0
  26. package/dist/include-path.test.js.map +1 -0
  27. package/dist/include-security-conformance.test.d.ts +1 -0
  28. package/dist/include-security-conformance.test.js +100 -0
  29. package/dist/include-security-conformance.test.js.map +1 -0
  30. package/dist/include-settings.d.ts +50 -0
  31. package/dist/include-settings.js +135 -0
  32. package/dist/include-settings.js.map +1 -0
  33. package/dist/include-settings.test.d.ts +1 -0
  34. package/dist/include-settings.test.js +191 -0
  35. package/dist/include-settings.test.js.map +1 -0
  36. package/dist/includes.d.ts +78 -0
  37. package/dist/includes.js +158 -0
  38. package/dist/includes.js.map +1 -0
  39. package/dist/includes.test.d.ts +1 -0
  40. package/dist/includes.test.js +202 -0
  41. package/dist/includes.test.js.map +1 -0
  42. package/dist/inline-text.d.ts +24 -0
  43. package/dist/inline-text.js +34 -0
  44. package/dist/inline-text.js.map +1 -0
  45. package/dist/inline-text.test.d.ts +1 -0
  46. package/dist/inline-text.test.js +32 -0
  47. package/dist/inline-text.test.js.map +1 -0
  48. package/dist/position-encoding.test.d.ts +1 -0
  49. package/dist/position-encoding.test.js +143 -0
  50. package/dist/position-encoding.test.js.map +1 -0
  51. package/dist/position.d.ts +82 -0
  52. package/dist/position.js +137 -0
  53. package/dist/position.js.map +1 -0
  54. package/dist/position.test.d.ts +1 -0
  55. package/dist/position.test.js +61 -0
  56. package/dist/position.test.js.map +1 -0
  57. package/dist/references.js +7 -0
  58. package/dist/references.js.map +1 -1
  59. package/dist/semantic.d.ts +14 -0
  60. package/dist/semantic.js +113 -27
  61. package/dist/semantic.js.map +1 -1
  62. package/dist/semantic.test.d.ts +1 -0
  63. package/dist/semantic.test.js +93 -0
  64. package/dist/semantic.test.js.map +1 -0
  65. package/dist/server.js +50 -23
  66. package/dist/server.js.map +1 -1
  67. package/package.json +2 -2
@@ -0,0 +1,202 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { resolveIncludes } from './includes.js';
4
+ // The graph-level half of PART 9 §19: recursion depth and total expanded byte
5
+ // size. Expectations follow the cross-engine include-conformance vectors
6
+ // `i03-*`, `i06-*` and `i11-*` in markup-carve/carve, which currently live only
7
+ // on the unmerged spec branch for markup-carve/carve#291.
8
+ /** In-memory resolver, matching the conformance suite's "virtual" mode. */
9
+ function virtualResolver(files, reads) {
10
+ return (includePath) => {
11
+ reads?.push(includePath);
12
+ const source = files[includePath];
13
+ if (source === undefined)
14
+ return { ok: false, id: includePath, denial: 'not-found' };
15
+ return { ok: true, id: includePath, source, bytes: Buffer.byteLength(source, 'utf8') };
16
+ };
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // Opt-in: the pass is inert until a host hands over a resolver
20
+ // ---------------------------------------------------------------------------
21
+ test('with no resolver the directive stays literal, with no warning and no dependency (i03-no-resolver-literal)', () => {
22
+ const result = resolveIncludes('See {{ child.crv }} here.');
23
+ assert.deepEqual(result.warnings, []);
24
+ assert.deepEqual(result.dependencies, []);
25
+ assert.equal(result.bytes, 0);
26
+ });
27
+ test('with no resolver a resolvable-looking target is still not reported (i03-no-deps-without-resolver)', () => {
28
+ const reads = [];
29
+ const result = resolveIncludes('{{ child }}', { sourcePath: '/doc.crv' });
30
+ assert.deepEqual(result.dependencies, []);
31
+ assert.deepEqual(reads, []);
32
+ });
33
+ // ---------------------------------------------------------------------------
34
+ // MUST 5: bound recursion depth
35
+ // ---------------------------------------------------------------------------
36
+ test('recursion stops at the depth limit and the unreached target is never read (i06-depth-limit)', () => {
37
+ const reads = [];
38
+ const result = resolveIncludes('{{ a }}', {
39
+ resolver: virtualResolver({ a: '{{ b }}', b: 'Deep.' }, reads),
40
+ maxDepth: 1,
41
+ });
42
+ assert.deepEqual(result.warnings.map((w) => ({ rule: w.rule, file: w.file })), [{ rule: 'include-depth', file: 'a' }]);
43
+ assert.deepEqual(result.dependencies, [
44
+ { id: 'a', resolved: true },
45
+ { id: 'b', resolved: false },
46
+ ]);
47
+ // The point of the bound: `b` is reported so a host can watch it, and is
48
+ // never handed to the resolver.
49
+ assert.deepEqual(reads, ['a']);
50
+ });
51
+ test('a deep chain is bounded rather than followed to its end', () => {
52
+ const files = {};
53
+ for (let i = 0; i < 40; i += 1)
54
+ files[`f${i}`] = `{{ f${i + 1} }}`;
55
+ files['f40'] = 'Bottom.';
56
+ const reads = [];
57
+ const result = resolveIncludes('{{ f0 }}', {
58
+ resolver: virtualResolver(files, reads),
59
+ maxDepth: 4,
60
+ });
61
+ assert.deepEqual(reads, ['f0', 'f1', 'f2', 'f3']);
62
+ assert.deepEqual(result.warnings.map((w) => w.rule), ['include-depth']);
63
+ });
64
+ // ---------------------------------------------------------------------------
65
+ // MUST 6: bound total expanded byte size
66
+ // ---------------------------------------------------------------------------
67
+ test('the byte budget stops expansion with a warning (i06-budget-limit)', () => {
68
+ const result = resolveIncludes('{{ a }}', {
69
+ resolver: virtualResolver({ a: 'too large' }),
70
+ maxBytes: 1,
71
+ });
72
+ assert.deepEqual(result.warnings.map((w) => w.rule), ['include-budget']);
73
+ assert.deepEqual(result.dependencies, [{ id: 'a', resolved: true }]);
74
+ });
75
+ test('the budget is charged per occurrence, so repeated includes amplify against it', () => {
76
+ // The include-bomb shape: one small file pulled in many times. A budget
77
+ // charged per DISTINCT file would let this through unbounded.
78
+ const child = 'x'.repeat(100);
79
+ const source = '{{ a }}\n\n'.repeat(20);
80
+ const reads = [];
81
+ const result = resolveIncludes(source, {
82
+ resolver: virtualResolver({ a: child }, reads),
83
+ maxBytes: 250,
84
+ });
85
+ // Two occurrences fit inside 250 bytes. The third is read and then rejected
86
+ // by the charge that takes the total past the budget; the remaining
87
+ // seventeen are rejected before the resolver is called at all, so the
88
+ // exhausted budget stops the reads as well as the expansion.
89
+ assert.equal(reads.length, 3);
90
+ assert.equal(result.bytes, 300);
91
+ assert.equal(result.warnings.filter((w) => w.rule === 'include-budget').length, 18);
92
+ });
93
+ test('a transitive fan-out is stopped by the budget', () => {
94
+ // a includes b twice, b includes c twice, and so on: 2^n growth from a
95
+ // handful of small files, which is the amplification the MUST names.
96
+ const files = {
97
+ a: '{{ b }}\n\n{{ b }}',
98
+ b: '{{ c }}\n\n{{ c }}',
99
+ c: '{{ d }}\n\n{{ d }}',
100
+ d: 'x'.repeat(200),
101
+ };
102
+ const result = resolveIncludes('{{ a }}', {
103
+ resolver: virtualResolver(files),
104
+ maxBytes: 600,
105
+ });
106
+ assert.ok(result.warnings.some((w) => w.rule === 'include-budget'));
107
+ });
108
+ // ---------------------------------------------------------------------------
109
+ // Cycles and dependency reporting
110
+ // ---------------------------------------------------------------------------
111
+ test('a two-file cycle is caught and attributed to the file that closed it (i06-cycle-two-file)', () => {
112
+ const result = resolveIncludes('{{ a }}', {
113
+ resolver: virtualResolver({ a: '{{ b }}', b: '{{ a }}' }),
114
+ });
115
+ assert.deepEqual(result.warnings.map((w) => ({ rule: w.rule, file: w.file })), [{ rule: 'include-cycle', file: 'b' }]);
116
+ assert.deepEqual(result.dependencies, [
117
+ { id: 'a', resolved: true },
118
+ { id: 'b', resolved: true },
119
+ ]);
120
+ });
121
+ test('a self-cycle is caught (i06-self-cycle)', () => {
122
+ const result = resolveIncludes('{{ a }}', { resolver: virtualResolver({ a: '{{ a }}' }) });
123
+ assert.deepEqual(result.warnings.map((w) => w.rule), ['include-cycle']);
124
+ });
125
+ test('cycle detection uses the recursion stack, so a diamond is not mistaken for a cycle', () => {
126
+ // a -> b -> d and a -> c -> d. `d` appears twice but never inside itself.
127
+ const result = resolveIncludes('{{ a }}', {
128
+ resolver: virtualResolver({ a: '{{ b }}\n\n{{ c }}', b: '{{ d }}', c: '{{ d }}', d: 'Leaf.' }),
129
+ });
130
+ assert.deepEqual(result.warnings, []);
131
+ assert.deepEqual(result.dependencies.map((d) => d.id), ['a', 'b', 'd', 'c']);
132
+ });
133
+ test('the same target twice is one dependency (i11-same-file-twice-once)', () => {
134
+ const result = resolveIncludes('{{ child }}\n\n{{ child }}', {
135
+ resolver: virtualResolver({ child: 'Body.' }),
136
+ });
137
+ assert.deepEqual(result.dependencies, [{ id: 'child', resolved: true }]);
138
+ });
139
+ test('a missing target is reported as an unresolved dependency (i11-missing-target-unresolved-dep)', () => {
140
+ const result = resolveIncludes('{{ present }}\n\n{{ absent }}', {
141
+ resolver: virtualResolver({ present: 'Here.' }),
142
+ });
143
+ assert.deepEqual(result.warnings.map((w) => w.rule), ['include-unresolved']);
144
+ assert.deepEqual(result.dependencies, [
145
+ { id: 'present', resolved: true },
146
+ { id: 'absent', resolved: false },
147
+ ]);
148
+ });
149
+ test('a resolver that throws yields a warning whose message carries no raw error text (i07-resolver-throws-no-leak)', () => {
150
+ const result = resolveIncludes('{{ a }}', {
151
+ resolver: () => {
152
+ throw new Error('ENOENT: open /home/someone/secret/a');
153
+ },
154
+ });
155
+ const [warning] = result.warnings;
156
+ assert.ok(warning);
157
+ assert.equal(warning.rule, 'include-unresolved');
158
+ assert.ok(!warning.message.includes('/home/someone'));
159
+ // The raw text is kept out of `message` but available for a log sink.
160
+ assert.ok(warning.detail?.includes('/home/someone'));
161
+ });
162
+ test('a warning carries the directive range and a 1-based line and column', () => {
163
+ const source = 'intro\n\n{{ absent }}\n';
164
+ const result = resolveIncludes(source, { resolver: virtualResolver({}) });
165
+ const [warning] = result.warnings;
166
+ assert.ok(warning);
167
+ assert.equal(warning.start, source.indexOf('{{ absent }}'));
168
+ assert.equal(warning.end, source.indexOf('{{ absent }}') + '{{ absent }}'.length);
169
+ assert.equal(warning.line, 3);
170
+ assert.equal(warning.column, 1);
171
+ });
172
+ test('a nested warning is anchored on the top-level directive of the open document', () => {
173
+ const source = 'intro\n\n{{ a }}\n';
174
+ const result = resolveIncludes(source, {
175
+ resolver: virtualResolver({ a: 'child text\n\n{{ absent }}' }),
176
+ });
177
+ const [warning] = result.warnings;
178
+ assert.ok(warning);
179
+ // The range must be valid in the document the client has open, and `file`
180
+ // says where the problem actually is.
181
+ assert.equal(warning.start, source.indexOf('{{ a }}'));
182
+ assert.equal(warning.file, 'a');
183
+ });
184
+ test('an unknown option warns and resolves nothing (i01-unknown-option-literal)', () => {
185
+ const reads = [];
186
+ const result = resolveIncludes('{{ child @nope:1 }}', {
187
+ resolver: virtualResolver({ child: 'text' }, reads),
188
+ });
189
+ assert.deepEqual(result.warnings.map((w) => w.rule), ['include-unknown-option']);
190
+ assert.deepEqual(result.dependencies, []);
191
+ assert.deepEqual(reads, []);
192
+ });
193
+ test('a directive inside a fence is not resolved even with a resolver active (i09-verbatim-fence-and-code-span)', () => {
194
+ const reads = [];
195
+ const result = resolveIncludes('```txt\n{{ child }}\n```\n\nUse `{{ child }}`.', {
196
+ resolver: virtualResolver({ child: 'expanded' }, reads),
197
+ });
198
+ assert.deepEqual(result.warnings, []);
199
+ assert.deepEqual(result.dependencies, []);
200
+ assert.deepEqual(reads, []);
201
+ });
202
+ //# sourceMappingURL=includes.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"includes.test.js","sourceRoot":"","sources":["../src/includes.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared inline-to-text fallback for the three plain-text walks (outline
3
+ * symbols, cross-reference matching, go-to-definition).
4
+ *
5
+ * Carve represents a typographic substitution as a `smart_punctuation` inline
6
+ * node rather than writing the glyph into the text buffer (carve spec PART 9
7
+ * §8). The node carries the resolved `kind`, the author's source run in
8
+ * `value`, and - for quotes, whose glyph is locale-dependent and chosen during
9
+ * parsing - the resolved character in `glyph`.
10
+ *
11
+ * The walks here used if/else-if chains with no final branch, so a node type
12
+ * they did not know about contributed nothing. That failed silently: every
13
+ * quote, apostrophe, dash and ellipsis vanished from the extracted text, and
14
+ * `# Don't repeat yourself` became `Dont repeat yourself` in outline symbols
15
+ * and in the text a crossref resolves against.
16
+ *
17
+ * Preference order is `glyph`, then the source run. Resolving `kind` through
18
+ * carve's glyph table would be marginally more faithful for dashes and
19
+ * ellipses, but the table is not exported from the package root, and it does
20
+ * not matter for what these walks feed: heading ids normalize typographic
21
+ * output back to ASCII anyway, and the three walks stay consistent with each
22
+ * other either way, which is what reference matching actually needs.
23
+ */
24
+ export declare function smartPunctuationText(node: unknown): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared inline-to-text fallback for the three plain-text walks (outline
3
+ * symbols, cross-reference matching, go-to-definition).
4
+ *
5
+ * Carve represents a typographic substitution as a `smart_punctuation` inline
6
+ * node rather than writing the glyph into the text buffer (carve spec PART 9
7
+ * §8). The node carries the resolved `kind`, the author's source run in
8
+ * `value`, and - for quotes, whose glyph is locale-dependent and chosen during
9
+ * parsing - the resolved character in `glyph`.
10
+ *
11
+ * The walks here used if/else-if chains with no final branch, so a node type
12
+ * they did not know about contributed nothing. That failed silently: every
13
+ * quote, apostrophe, dash and ellipsis vanished from the extracted text, and
14
+ * `# Don't repeat yourself` became `Dont repeat yourself` in outline symbols
15
+ * and in the text a crossref resolves against.
16
+ *
17
+ * Preference order is `glyph`, then the source run. Resolving `kind` through
18
+ * carve's glyph table would be marginally more faithful for dashes and
19
+ * ellipses, but the table is not exported from the package root, and it does
20
+ * not matter for what these walks feed: heading ids normalize typographic
21
+ * output back to ASCII anyway, and the three walks stay consistent with each
22
+ * other either way, which is what reference matching actually needs.
23
+ */
24
+ export function smartPunctuationText(node) {
25
+ if (typeof node !== 'object' || node === null)
26
+ return '';
27
+ const candidate = node;
28
+ if (candidate.type !== 'smart_punctuation')
29
+ return '';
30
+ if (typeof candidate.glyph === 'string')
31
+ return candidate.glyph;
32
+ return typeof candidate.value === 'string' ? candidate.value : '';
33
+ }
34
+ //# sourceMappingURL=inline-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-text.js","sourceRoot":"","sources":["../src/inline-text.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAa;IAChD,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,EAAE,CAAA;IACxD,MAAM,SAAS,GAAG,IAA4D,CAAA;IAC9E,IAAI,SAAS,CAAC,IAAI,KAAK,mBAAmB;QAAE,OAAO,EAAE,CAAA;IACrD,IAAI,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC,KAAK,CAAA;IAC/D,OAAO,OAAO,SAAS,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;AACnE,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { smartPunctuationText } from './inline-text.js';
4
+ // The nodes below are written literally rather than produced by parse(), so the
5
+ // tests hold whether or not the installed @markup-carve/carve emits
6
+ // smart_punctuation yet. That is the point: the regression this guards against
7
+ // arrives with a dependency bump, and a test that only exercises the current
8
+ // dependency would pass right up until the day it mattered.
9
+ test('a quote node contributes its resolved glyph', () => {
10
+ // Quote glyphs are locale-dependent and fixed during parsing, so the node
11
+ // carries the character rather than leaving it to a kind lookup.
12
+ assert.equal(smartPunctuationText({ type: 'smart_punctuation', kind: 'right_single_quote', value: "'", glyph: '’' }), '’');
13
+ });
14
+ test('a node without a glyph falls back to the author source run', () => {
15
+ assert.equal(smartPunctuationText({ type: 'smart_punctuation', kind: 'ellipsis', value: '...' }), '...');
16
+ assert.equal(smartPunctuationText({ type: 'smart_punctuation', kind: 'em_dash', value: '---' }), '---');
17
+ });
18
+ test('other node types contribute nothing', () => {
19
+ // The helper sits in a final else, so every unrecognized node reaches it. It
20
+ // must stay silent for anything that is not smart punctuation, matching the
21
+ // previous behavior of those chains.
22
+ assert.equal(smartPunctuationText({ type: 'soft_break' }), '');
23
+ assert.equal(smartPunctuationText({ type: 'text', value: 'x' }), '');
24
+ });
25
+ test('malformed input does not throw', () => {
26
+ assert.equal(smartPunctuationText(null), '');
27
+ assert.equal(smartPunctuationText(undefined), '');
28
+ assert.equal(smartPunctuationText('not a node'), '');
29
+ assert.equal(smartPunctuationText({ type: 'smart_punctuation' }), '');
30
+ assert.equal(smartPunctuationText({ type: 'smart_punctuation', value: 42 }), '');
31
+ });
32
+ //# sourceMappingURL=inline-text.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inline-text.test.js","sourceRoot":"","sources":["../src/inline-text.test.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,oBAAoB,CAAA;AACvC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AAEvD,gFAAgF;AAChF,oEAAoE;AACpE,+EAA+E;AAC/E,6EAA6E;AAC7E,4DAA4D;AAE5D,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;IACvD,0EAA0E;IAC1E,iEAAiE;IACjE,MAAM,CAAC,KAAK,CACV,oBAAoB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,oBAAoB,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EACvG,GAAG,CACJ,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,4DAA4D,EAAE,GAAG,EAAE;IACtE,MAAM,CAAC,KAAK,CACV,oBAAoB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EACnF,KAAK,CACN,CAAA;IACD,MAAM,CAAC,KAAK,CACV,oBAAoB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAClF,KAAK,CACN,CAAA;AACH,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,qCAAqC,EAAE,GAAG,EAAE;IAC/C,6EAA6E;IAC7E,4EAA4E;IAC5E,qCAAqC;IACrC,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;IAC9D,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AACtE,CAAC,CAAC,CAAA;AAEF,IAAI,CAAC,gCAAgC,EAAE,GAAG,EAAE;IAC1C,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAA;IAC5C,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAA;IACjD,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,CAAA;IACpD,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;IACrE,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC,CAAA;AAClF,CAAC,CAAC,CAAA"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,143 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { hoverAt } from './hover.js';
4
+ import { semanticTokens } from './semantic.js';
5
+ import { astColumnToCharacter, characterToAstColumn, engineColumnUnit, setEngineColumnUnit, CODEPOINT_PROBE_END_COLUMN, COLUMN_PROBE, probeEndColumn, } from './position.js';
6
+ function hoverText(hover) {
7
+ const contents = hover.contents;
8
+ return contents.value ?? String(hover.contents);
9
+ }
10
+ // Every assertion below is written against the SOURCE, not against the parser's
11
+ // numbers: an LSP character is a UTF-16 index into the line, so `indexOf` is
12
+ // the answer a client expects no matter which unit the installed parser counts
13
+ // in. That is what makes these hold across the carve-js release that switched
14
+ // columns from UTF-16 code units to codepoints (carve-js#447) - the release
15
+ // this server's `^0.1.2` range picks up without a change here.
16
+ const LINE = '\u{1F600} *bold* tail';
17
+ const BOLD_START = LINE.indexOf('*');
18
+ const BOLD_END = LINE.lastIndexOf('*') + 1;
19
+ // The unit the PINNED engine reports, asserted directly.
20
+ //
21
+ // The test above is deliberately unit-agnostic, so a release that changes the
22
+ // unit passes it - which is how the change got in last time. This one names the
23
+ // answer, so the change arrives as a failing assertion instead of as correct
24
+ // positions on ASCII and wrong ones on every emoji.
25
+ //
26
+ // That is exactly what happened: this assertion read `utf16` while the pin sat
27
+ // at `0.1.2`, and raising the pin to `0.1.3` turned it red. `0.1.3` carries
28
+ // carve-js#447, which counts codepoints and so conforms to spec PART 12
29
+ // section 4. The expectation is flipped here rather than relaxed, because an
30
+ // assertion that accepts either unit is the shape that let the change through
31
+ // unnoticed the first time.
32
+ //
33
+ // The conversions in position.js have always had both branches; until this
34
+ // release the `codepoint` one was unreachable with the installed engine. It is
35
+ // the live branch now, and the unit-agnostic tests above cover it: they derive
36
+ // every expectation from `indexOf` on the SOURCE, so they would fail if the
37
+ // conversion returned the parser's raw codepoint column to an LSP client.
38
+ test('the pinned engine counts codepoints', () => {
39
+ assert.equal(engineColumnUnit(), 'codepoint');
40
+ });
41
+ // This test used to retype the probe string and the constant, and then assert
42
+ // that engineColumnUnit() agreed with a recomputation of its own expression:
43
+ //
44
+ // const endColumn = parse('\u{1F600}x').children[0]?.pos?.endColumn
45
+ // assert.equal(engineColumnUnit(), endColumn === 3 ? 'codepoint' : 'utf16')
46
+ //
47
+ // Both sides read the same value through the same expression, so they agreed
48
+ // for every value the engine can return - including `undefined`, where both
49
+ // take the `utf16` branch and the "answer" is the catch-all rather than a
50
+ // measurement. Two mutations, both measured against the old assertion, both
51
+ // leaving the whole suite green at 134 of 134:
52
+ //
53
+ // COLUMN_PROBE = 'abc' a BMP-only probe has the same end column under
54
+ // either unit, so the classifier can no longer tell
55
+ // them apart. The test reparsed its own emoji probe
56
+ // and never saw it.
57
+ // the classifier reads a property that does not exist, so every
58
+ // classification is the fallback. The test reparsed
59
+ // and got a real number, so it agreed with the
60
+ // fallback and reported nothing.
61
+ //
62
+ // What follows checks three things separately, against values derived from the
63
+ // probe rather than copied from it, and consumes the classifier's own reading
64
+ // rather than making a second one.
65
+ test('the probe can tell the two units apart', () => {
66
+ // A string is indexed in UTF-16 code units and iterated in codepoints, so a
67
+ // probe whose two lengths are equal reports the same end column under either
68
+ // unit and classifies nothing. This is a property of the probe alone, with no
69
+ // engine involved.
70
+ assert.notEqual(COLUMN_PROBE.length, [...COLUMN_PROBE].length, 'the probe has no astral character, so both units give it the same end column');
71
+ // And the constant the classifier compares against is that probe's codepoint
72
+ // end column - derived here, hard-coded there.
73
+ assert.equal(CODEPOINT_PROBE_END_COLUMN, [...COLUMN_PROBE].length + 1);
74
+ });
75
+ test('the installed parser answers the probe, rather than failing into the default', () => {
76
+ // engineColumnUnit() answers `utf16` when the parse throws or the position is
77
+ // absent. That is the right thing for a server not to crash on, and the wrong
78
+ // thing to accept silently in CI: a classification nobody measured reads
79
+ // exactly like one that was.
80
+ //
81
+ // probeEndColumn() rather than a reparse here: this is the reading the
82
+ // classifier actually makes, so a classifier that stops reading a position
83
+ // fails this. A reparse would agree with the fallback and report nothing.
84
+ const endColumn = probeEndColumn();
85
+ assert.equal(typeof endColumn, 'number', 'the installed engine placed no position on the probe, so the unit below is a default, not a reading');
86
+ // Only two answers are possible for this probe, one per unit. Anything else
87
+ // means the engine is not counting either of the units this server knows how
88
+ // to convert between.
89
+ assert.ok(endColumn === [...COLUMN_PROBE].length + 1 || endColumn === COLUMN_PROBE.length + 1, `the probe's end column is ${endColumn}, which is neither its codepoint length + 1 ` +
90
+ `(${[...COLUMN_PROBE].length + 1}) nor its UTF-16 length + 1 (${COLUMN_PROBE.length + 1})`);
91
+ assert.equal(engineColumnUnit(), endColumn === [...COLUMN_PROBE].length + 1 ? 'codepoint' : 'utf16');
92
+ });
93
+ test('an emoji before a construct does not shift its hover range', () => {
94
+ const hover = hoverAt(LINE, { line: 0, character: BOLD_START + 1 });
95
+ assert.ok(hover, 'expected a hover inside the bold span');
96
+ assert.match(hoverText(hover), /Bold/);
97
+ assert.deepEqual(hover.range, {
98
+ start: { line: 0, character: BOLD_START },
99
+ end: { line: 0, character: BOLD_END },
100
+ });
101
+ });
102
+ test('a cursor placed by UTF-16 character finds the node the client pointed at', () => {
103
+ // The character an editor sends counts UTF-16 units. Compared against a
104
+ // codepoint column without converting, this cursor lands one position early
105
+ // and hovers the surrounding text instead of the bold span.
106
+ const hover = hoverAt(LINE, { line: 0, character: BOLD_END - 2 });
107
+ assert.ok(hover);
108
+ assert.match(hoverText(hover), /Bold/);
109
+ });
110
+ test('semantic tokens land on the UTF-16 columns the client indexes by', () => {
111
+ const tokens = semanticTokens(LINE);
112
+ const bold = tokens.find((token) => token.character === BOLD_START);
113
+ assert.ok(bold, `expected a token at character ${BOLD_START}, got ${JSON.stringify(tokens)}`);
114
+ assert.equal(bold.length, BOLD_END - BOLD_START);
115
+ });
116
+ test('both parser readings map the same construct to the same LSP character', () => {
117
+ // The installed parser can only show one reading, and it is the one whose
118
+ // conversion is the identity - so the other branch is driven explicitly
119
+ // rather than left to run for the first time after a dependency bump.
120
+ // In `LINE` the opening `*` is codepoint column 3 and UTF-16 column 4,
121
+ // because the emoji is one codepoint and two code units.
122
+ try {
123
+ setEngineColumnUnit('codepoint');
124
+ assert.equal(astColumnToCharacter(LINE, 3), BOLD_START);
125
+ assert.equal(characterToAstColumn(LINE, BOLD_START), 3);
126
+ setEngineColumnUnit('utf16');
127
+ assert.equal(astColumnToCharacter(LINE, 4), BOLD_START);
128
+ assert.equal(characterToAstColumn(LINE, BOLD_START), 4);
129
+ }
130
+ finally {
131
+ setEngineColumnUnit(undefined);
132
+ }
133
+ });
134
+ test('a BMP-only document is unaffected either way', () => {
135
+ const line = 'a *bold* tail';
136
+ const hover = hoverAt(line, { line: 0, character: 3 });
137
+ assert.ok(hover);
138
+ assert.deepEqual(hover.range, {
139
+ start: { line: 0, character: line.indexOf('*') },
140
+ end: { line: 0, character: line.lastIndexOf('*') + 1 },
141
+ });
142
+ });
143
+ //# sourceMappingURL=position-encoding.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"position-encoding.test.js","sourceRoot":"","sources":["../src/position-encoding.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Converts between the units Carve positions and LSP positions count.
3
+ *
4
+ * A Carve AST position counts UNICODE CODEPOINTS (spec PART 12 §4, chosen so
5
+ * an index always lands on a character boundary). An LSP position counts
6
+ * UTF-16 code units unless the client and server negotiate `positionEncoding`,
7
+ * which this server does not do, so UTF-16 is what every client here expects.
8
+ *
9
+ * The two units agree on the entire BMP and diverge on every astral character:
10
+ * one emoji earlier in the line shifts every LSP column after it by one. So a
11
+ * position read off the AST cannot be handed to a client, nor a position from
12
+ * a client compared against the AST, without passing through here.
13
+ */
14
+ /**
15
+ * A 1-based codepoint column, as an AST position reports it, to the 0-based
16
+ * UTF-16 character an LSP range carries.
17
+ */
18
+ export declare function codepointColumnToUtf16(lineText: string, column: number): number;
19
+ /**
20
+ * The 0-based UTF-16 character an LSP request carries, to the 1-based codepoint
21
+ * column an AST position can be compared against.
22
+ *
23
+ * A character index pointing into the middle of a surrogate pair resolves to
24
+ * the column after that pair. There is no column for half a character, and a
25
+ * client only produces one by placing a cursor inside one.
26
+ */
27
+ export declare function utf16CharToCodepointColumn(lineText: string, character: number): number;
28
+ /**
29
+ * The document's lines, line endings normalized, indexed 0-based - the form
30
+ * both conversions above need to see a line's characters.
31
+ */
32
+ export declare function sourceLines(source: string): string[];
33
+ /**
34
+ * Which unit the installed parser's columns are in.
35
+ *
36
+ * This is not a constant, because the dependency range spans both answers.
37
+ * Positions arrived in @markup-carve/carve counting UTF-16 code units, which
38
+ * is what a JavaScript string is indexed by; carve-js#447 then made them count
39
+ * codepoints, to conform to the spec. Both satisfy `^0.1.2`, so a build of this
40
+ * server installed today and the same build installed after that release are
41
+ * handed different numbers for the same document, and nothing in the package
42
+ * metadata distinguishes them.
43
+ *
44
+ * Rather than guess, ask: one astral character is two UTF-16 code units and one
45
+ * codepoint, so a two-character probe reports a different end column under each
46
+ * unit. The answer is cached - it cannot change within a process.
47
+ */
48
+ export declare const COLUMN_PROBE = "\uD83D\uDE00x";
49
+ export declare const CODEPOINT_PROBE_END_COLUMN = 3;
50
+ /**
51
+ * The raw reading the classification is made from: the probe's end column as
52
+ * the installed parser reports it, or `undefined` when the parser cannot answer
53
+ * at all.
54
+ *
55
+ * Split out and exported so the guard test can consume THE READING the
56
+ * classifier uses, instead of computing its own copy of it. A test that reparses
57
+ * the probe itself agrees with `engineColumnUnit` for every value including
58
+ * `undefined`, so it cannot see the difference between a measurement and the
59
+ * fallback below - which is the only thing worth checking here.
60
+ */
61
+ export declare function probeEndColumn(): number | undefined;
62
+ export declare function engineColumnUnit(): 'codepoint' | 'utf16';
63
+ /**
64
+ * Test seam: pin the unit, or pass `undefined` to probe again.
65
+ *
66
+ * The installed parser can only demonstrate one of the two readings, and it is
67
+ * the one whose conversion is the identity - so without this the codepoint
68
+ * branch would ship untested and unrun until a dependency bump made it live.
69
+ */
70
+ export declare function setEngineColumnUnit(unit: 'codepoint' | 'utf16' | undefined): void;
71
+ /**
72
+ * A 1-based column as the installed parser reports it, to the 0-based UTF-16
73
+ * character an LSP range carries. Use this rather than the raw conversion:
74
+ * under a UTF-16 parser the columns already are LSP characters, and converting
75
+ * them would introduce the very shift it is meant to remove.
76
+ */
77
+ export declare function astColumnToCharacter(lineText: string, column: number): number;
78
+ /**
79
+ * The 0-based UTF-16 character an LSP request carries, to the 1-based column
80
+ * the installed parser's positions can be compared against.
81
+ */
82
+ export declare function characterToAstColumn(lineText: string, character: number): number;