@effected/source 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/LineIndex.js +258 -0
- package/README.md +61 -2
- package/index.d.ts +359 -0
- package/index.js +3 -0
- package/package.json +44 -5
- package/tsdoc-metadata.json +11 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 C. Spencer Beggs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LineIndex.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// src/LineIndex.ts
|
|
2
|
+
const LF = 10;
|
|
3
|
+
const CR = 13;
|
|
4
|
+
const LS = 8232;
|
|
5
|
+
const PS = 8233;
|
|
6
|
+
const requireInteger = (name, value) => {
|
|
7
|
+
if (!Number.isInteger(value)) throw new RangeError(`LineIndex: ${name} must be an integer, received ${value}`);
|
|
8
|
+
};
|
|
9
|
+
const requireLength = (length) => {
|
|
10
|
+
requireInteger("length", length);
|
|
11
|
+
if (length < 0) throw new RangeError(`LineIndex: length must not be negative, received ${length}`);
|
|
12
|
+
};
|
|
13
|
+
const clamp = (value, low, high) => Math.min(Math.max(value, low), high);
|
|
14
|
+
/**
|
|
15
|
+
* A line table over one source text, built once and queried many times:
|
|
16
|
+
* offset to `{ line, character }` and back, the number of lines, and where
|
|
17
|
+
* each line starts and ends.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Positions are zero-based and `character` counts UTF-16 code units, LSP's
|
|
21
|
+
* default encoding (see {@link SourcePosition}). Building the index is one
|
|
22
|
+
* forward scan, `O(n)`; every lookup is a binary search, `O(log n)`, so build
|
|
23
|
+
* one per text and reuse it rather than rescanning for every diagnostic.
|
|
24
|
+
*
|
|
25
|
+
* Out-of-range input clamps instead of throwing, following
|
|
26
|
+
* `vscode-languageserver-textdocument`'s `TextDocument`, which most LSP
|
|
27
|
+
* servers already use:
|
|
28
|
+
*
|
|
29
|
+
* - an offset below `0` reads as `0`, and one past the end as `text.length`;
|
|
30
|
+
* - an offset inside a two-character terminator (the `\n` of a `\r\n`) reads
|
|
31
|
+
* as the end of its line, because no position can name the middle of a
|
|
32
|
+
* terminator;
|
|
33
|
+
* - a `character` past the end of its line reads as the end of the line's
|
|
34
|
+
* text, before its terminator, and a negative one as the line's start;
|
|
35
|
+
* - a `line` below `0` reads as offset `0`, and one past the last line as
|
|
36
|
+
* `text.length`.
|
|
37
|
+
*
|
|
38
|
+
* A value that is not an integer (`NaN`, `1.5`, `Infinity`) is a programmer
|
|
39
|
+
* error, not out-of-range input, and throws a `RangeError`: clamping `NaN`
|
|
40
|
+
* would silently answer "line 0".
|
|
41
|
+
*
|
|
42
|
+
* Every text has at least one line. The empty text has one empty line, and a
|
|
43
|
+
* text ending in a terminator has an empty last line after it, so `"a\n"` has
|
|
44
|
+
* two lines, as an editor shows it.
|
|
45
|
+
*
|
|
46
|
+
* `character` can name the low half of a surrogate pair; LSP leaves that to
|
|
47
|
+
* the client, and so does this index. It offers no code-point encoding:
|
|
48
|
+
* every LSP client must accept UTF-16, and each kit format package reports
|
|
49
|
+
* UTF-16 offsets.
|
|
50
|
+
*
|
|
51
|
+
* There is no shared-instance contract. Nothing checks `instanceof LineIndex`,
|
|
52
|
+
* and every value it returns is a plain record, so two installed copies of
|
|
53
|
+
* this package interoperate at runtime. At the type level the class does
|
|
54
|
+
* not: its `private` members make TypeScript compare it nominally, so a
|
|
55
|
+
* `LineIndex` from one copy is not assignable to another copy's `LineIndex`.
|
|
56
|
+
* Type a signature that takes or returns an index with the structural
|
|
57
|
+
* {@link SourceIndex} interface this class implements, never with the class.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { LineIndex } from "@effected/source";
|
|
62
|
+
*
|
|
63
|
+
* const index = LineIndex.make("name: kit\r\nversion: 1\n");
|
|
64
|
+
* console.log(index.lineCount);
|
|
65
|
+
* // => 3
|
|
66
|
+
* console.log(index.positionAt(11));
|
|
67
|
+
* // => { line: 1, character: 0 }
|
|
68
|
+
* console.log(index.offsetAt({ line: 1, character: 9 }));
|
|
69
|
+
* // => 20
|
|
70
|
+
* console.log(index.lineText(1));
|
|
71
|
+
* // => "version: 1"
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @public
|
|
75
|
+
*/
|
|
76
|
+
var LineIndex = class LineIndex {
|
|
77
|
+
text;
|
|
78
|
+
lineBreaks;
|
|
79
|
+
starts;
|
|
80
|
+
constructor(text, lineBreaks, starts) {
|
|
81
|
+
this.text = text;
|
|
82
|
+
this.lineBreaks = lineBreaks;
|
|
83
|
+
this.starts = starts;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the line table for `text` with one forward scan.
|
|
87
|
+
*
|
|
88
|
+
* @param text - The source text.
|
|
89
|
+
* @param options - Which characters end a line; see {@link LineIndexOptions}.
|
|
90
|
+
* @returns The index.
|
|
91
|
+
*/
|
|
92
|
+
static make(text, options) {
|
|
93
|
+
const lineBreaks = options?.lineBreaks ?? "lsp";
|
|
94
|
+
const starts = [0];
|
|
95
|
+
const length = text.length;
|
|
96
|
+
for (let i = 0; i < length; i++) {
|
|
97
|
+
const code = text.charCodeAt(i);
|
|
98
|
+
if (code === LF) starts.push(i + 1);
|
|
99
|
+
else if (code === CR) {
|
|
100
|
+
if (i + 1 < length && text.charCodeAt(i + 1) === LF) {
|
|
101
|
+
starts.push(i + 2);
|
|
102
|
+
i++;
|
|
103
|
+
} else if (lineBreaks !== "lf") starts.push(i + 1);
|
|
104
|
+
} else if ((code === LS || code === PS) && lineBreaks === "unicode") starts.push(i + 1);
|
|
105
|
+
}
|
|
106
|
+
return new LineIndex(text, lineBreaks, starts);
|
|
107
|
+
}
|
|
108
|
+
/** The number of lines, at least `1`. */
|
|
109
|
+
get lineCount() {
|
|
110
|
+
return this.starts.length;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The zero-based line that contains `offset`. An offset at a terminator
|
|
114
|
+
* belongs to the line the terminator ends.
|
|
115
|
+
*
|
|
116
|
+
* @param offset - A UTF-16 offset, clamped to `[0, text.length]`.
|
|
117
|
+
* @returns The line number.
|
|
118
|
+
*/
|
|
119
|
+
lineAt(offset) {
|
|
120
|
+
requireInteger("offset", offset);
|
|
121
|
+
const target = clamp(offset, 0, this.text.length);
|
|
122
|
+
let low = 0;
|
|
123
|
+
let high = this.starts.length - 1;
|
|
124
|
+
while (low < high) {
|
|
125
|
+
const mid = low + high + 1 >> 1;
|
|
126
|
+
if (this.starts[mid] <= target) low = mid;
|
|
127
|
+
else high = mid - 1;
|
|
128
|
+
}
|
|
129
|
+
return low;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* The offset of the first character of `line`.
|
|
133
|
+
*
|
|
134
|
+
* @param line - A zero-based line, clamped to `[0, lineCount - 1]`.
|
|
135
|
+
* @returns The line's start offset.
|
|
136
|
+
*/
|
|
137
|
+
lineStart(line) {
|
|
138
|
+
return this.starts[this.clampLine(line)];
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The offset just past the last character of `line`'s text, which is
|
|
142
|
+
* where its terminator starts, or `text.length` on the last line.
|
|
143
|
+
*
|
|
144
|
+
* @param line - A zero-based line, clamped to `[0, lineCount - 1]`.
|
|
145
|
+
* @returns The line's end offset, excluding its terminator.
|
|
146
|
+
*/
|
|
147
|
+
lineEnd(line) {
|
|
148
|
+
return this.endOf(this.clampLine(line));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* The text of `line`, without its terminator.
|
|
152
|
+
*
|
|
153
|
+
* @param line - A zero-based line, clamped to `[0, lineCount - 1]`.
|
|
154
|
+
* @returns The line's text.
|
|
155
|
+
*/
|
|
156
|
+
lineText(line) {
|
|
157
|
+
const index = this.clampLine(line);
|
|
158
|
+
return this.text.slice(this.starts[index], this.endOf(index));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* The zero-based `{ line, character }` of `offset`.
|
|
162
|
+
*
|
|
163
|
+
* @param offset - A UTF-16 offset, clamped to `[0, text.length]`; one
|
|
164
|
+
* inside a `\r\n` reads as the end of its line.
|
|
165
|
+
* @returns The position.
|
|
166
|
+
*/
|
|
167
|
+
positionAt(offset) {
|
|
168
|
+
const line = this.lineAt(offset);
|
|
169
|
+
const target = clamp(offset, 0, this.text.length);
|
|
170
|
+
const start = this.starts[line];
|
|
171
|
+
return {
|
|
172
|
+
line,
|
|
173
|
+
character: Math.min(target, this.endOf(line)) - start
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* The offset of a zero-based `{ line, character }`.
|
|
178
|
+
*
|
|
179
|
+
* @param position - The position. A `line` past the last reads as
|
|
180
|
+
* `text.length` and one below `0` as `0`; a `character` past the line's
|
|
181
|
+
* text reads as the line's end, before its terminator.
|
|
182
|
+
* @returns The UTF-16 offset.
|
|
183
|
+
*/
|
|
184
|
+
offsetAt(position) {
|
|
185
|
+
requireInteger("line", position.line);
|
|
186
|
+
requireInteger("character", position.character);
|
|
187
|
+
if (position.line < 0) return 0;
|
|
188
|
+
if (position.line >= this.starts.length) return this.text.length;
|
|
189
|
+
const start = this.starts[position.line];
|
|
190
|
+
const end = this.endOf(position.line);
|
|
191
|
+
return clamp(start + position.character, start, end);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The {@link SourceSpan} (LSP `Range`) of an offset range.
|
|
195
|
+
*
|
|
196
|
+
* @param range - The range; its `length` must not be negative.
|
|
197
|
+
* @returns The start and exclusive end positions.
|
|
198
|
+
*/
|
|
199
|
+
spanOf(range) {
|
|
200
|
+
requireLength(range.length);
|
|
201
|
+
return {
|
|
202
|
+
start: this.positionAt(range.offset),
|
|
203
|
+
end: this.positionAt(range.offset + range.length)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The {@link SourceRange} of a {@link SourceSpan}, after each position is
|
|
208
|
+
* clamped as {@link LineIndex.offsetAt} clamps it.
|
|
209
|
+
*
|
|
210
|
+
* @param span - The span; `end` must not precede `start`.
|
|
211
|
+
* @returns The offset and length.
|
|
212
|
+
*/
|
|
213
|
+
rangeOf(span) {
|
|
214
|
+
const start = this.offsetAt(span.start);
|
|
215
|
+
const end = this.offsetAt(span.end);
|
|
216
|
+
if (end < start) throw new RangeError(`LineIndex: span end ${span.end.line}:${span.end.character} precedes its start ${span.start.line}:${span.start.character}`);
|
|
217
|
+
return {
|
|
218
|
+
offset: start,
|
|
219
|
+
length: end - start
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The {@link SourceLocation} of a range: the range itself, unchanged,
|
|
224
|
+
* with the position of its start. The shape a diagnostic carries.
|
|
225
|
+
*
|
|
226
|
+
* @param range - The range; its `length` must be a non-negative integer,
|
|
227
|
+
* as {@link LineIndex.spanOf} requires.
|
|
228
|
+
* @returns `offset`, `length`, `line` and `character`.
|
|
229
|
+
*/
|
|
230
|
+
locationOf(range) {
|
|
231
|
+
requireLength(range.length);
|
|
232
|
+
const { line, character } = this.positionAt(range.offset);
|
|
233
|
+
return {
|
|
234
|
+
offset: range.offset,
|
|
235
|
+
length: range.length,
|
|
236
|
+
line,
|
|
237
|
+
character
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Where `line`'s text ends, derived from the next line's start rather
|
|
242
|
+
* than stored: the next start minus the terminator's length, two for a
|
|
243
|
+
* `\r\n` and one for every other terminator, or `text.length` on the
|
|
244
|
+
* last line. Keeping one table halves the index's memory; the lookup
|
|
245
|
+
* stays `O(1)` after the `O(log n)` search that found the line.
|
|
246
|
+
*/
|
|
247
|
+
endOf(line) {
|
|
248
|
+
const next = this.starts[line + 1];
|
|
249
|
+
if (next === void 0) return this.text.length;
|
|
250
|
+
return this.text.charCodeAt(next - 1) === LF && this.text.charCodeAt(next - 2) === CR ? next - 2 : next - 1;
|
|
251
|
+
}
|
|
252
|
+
clampLine(line) {
|
|
253
|
+
requireInteger("line", line);
|
|
254
|
+
return clamp(line, 0, this.starts.length - 1);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export { LineIndex };
|
package/README.md
CHANGED
|
@@ -1,3 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @effected/source
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@effected/source)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
|
|
8
|
+
Source-text positions as plain values: a line index that turns an offset into an LSP-style zero-based line and character and back, and the range, position and diagnostic shapes the `@effected` format packages share. No service, no platform, no `effect`, and no dependency at all.
|
|
9
|
+
|
|
10
|
+
> **Pre-`1.0.0`.** This package is part of the `@effected/*` kit, still in `0.x`
|
|
11
|
+
> development. It has no dependencies, not even `effect`.
|
|
12
|
+
>
|
|
13
|
+
> **Stability: unstable.** This package's API surface is not yet considered
|
|
14
|
+
> complete and may change across `0.x` releases. Pin an exact version. Full
|
|
15
|
+
> policy: [release strategy](https://github.com/spencerbeggs/effected#release-strategy).
|
|
16
|
+
|
|
17
|
+
## Why @effected/source
|
|
18
|
+
|
|
19
|
+
A parser reports where something went wrong as an offset into the text. An editor, a language server and a person all want a line and a column. The conversion looks like a five-line loop, and that loop is easy to get subtly wrong: which characters end a line, what an offset between the `\r` and `\n` of a Windows line ending means, and whether a column counts code units or characters. This package is the conversion written once, by the rules the Language Server Protocol uses.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @effected/source
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pnpm add @effected/source
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Node.js >=24.11.0. No dependencies and no peers. ESM-only: import it from an ES module.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { LineIndex } from "@effected/source";
|
|
37
|
+
|
|
38
|
+
const index = LineIndex.make("name: kit\r\nversion: 1\n");
|
|
39
|
+
|
|
40
|
+
index.positionAt(11);
|
|
41
|
+
// { line: 1, character: 0 }
|
|
42
|
+
|
|
43
|
+
index.offsetAt({ line: 1, character: 9 });
|
|
44
|
+
// 20
|
|
45
|
+
|
|
46
|
+
index.spanOf({ offset: 11, length: 7 });
|
|
47
|
+
// { start: { line: 1, character: 0 }, end: { line: 1, character: 7 } }
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- **`LineIndex.make(text, options?)`** builds the line table once; every query after that is a binary search. `positionAt` and `offsetAt` convert both ways, `lineCount`, `lineAt`, `lineStart`, `lineEnd` and `lineText` describe the lines, and `spanOf`, `rangeOf` and `locationOf` convert ranges.
|
|
51
|
+
- **Positions are zero-based** and `character` counts UTF-16 code units, LSP's default. Add 1 to both for a person.
|
|
52
|
+
- **Lines end at `\n`, `\r\n` or a bare `\r`** by default. `lineBreaks: "lf"` ends them at `\n` only, and `lineBreaks: "unicode"` also at U+2028 and U+2029.
|
|
53
|
+
- **Out-of-range input clamps** to the nearest valid position, as `vscode-languageserver-textdocument` does. A value that is not an integer throws a `RangeError`.
|
|
54
|
+
- **`SourcePosition`, `SourceRange`, `SourceSpan`, `SourceLocation` and `SourceDiagnostic`** are structural interfaces, so the diagnostics and ranges of `@effected/jsonc`, `@effected/yaml`, `@effected/toml` and `@effected/markdown` can be read through them as they are.
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
The guide and the API reference are at [effected.spencerbeg.gs/source](https://effected.spencerbeg.gs/source).
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
[MIT](LICENSE)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A zero-based line and character in a source text, the shape of an LSP
|
|
3
|
+
* `Position`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Both fields count from `0`. `character` counts UTF-16 code units from the
|
|
7
|
+
* start of the line, which is LSP's default position encoding and what a
|
|
8
|
+
* JavaScript string index already is, so an astral character such as an emoji
|
|
9
|
+
* occupies two. A line is ended by `\n`, `\r\n` or a bare `\r` unless the
|
|
10
|
+
* {@link LineIndex} that produced the position was built with another
|
|
11
|
+
* {@link (LineBreaks:type)} rule.
|
|
12
|
+
*
|
|
13
|
+
* This is a structural interface, not a class: any `{ line, character }`
|
|
14
|
+
* record satisfies it, so the kit's format packages can return their own
|
|
15
|
+
* classes and still be read through it, and two installed copies of this
|
|
16
|
+
* package never disagree about what a position is.
|
|
17
|
+
*
|
|
18
|
+
* A human reader counts from 1. Add 1 to both fields when printing a
|
|
19
|
+
* position for a person, and keep the structured value zero-based.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
interface SourcePosition {
|
|
24
|
+
/** The zero-based line number. */
|
|
25
|
+
readonly line: number;
|
|
26
|
+
/** The zero-based offset within the line, in UTF-16 code units. */
|
|
27
|
+
readonly character: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A span of a source text as a start offset and a length, both in UTF-16 code
|
|
31
|
+
* units: the characters `[offset, offset + length)`.
|
|
32
|
+
*
|
|
33
|
+
* @remarks
|
|
34
|
+
* The shape every kit format package uses for a range (`JsoncRange`,
|
|
35
|
+
* `YamlRange`, `TomlRange`, `MarkdownRange`) and for the positional part of an
|
|
36
|
+
* edit, so any of them satisfies this interface as it stands. A `length` of
|
|
37
|
+
* `0` is an empty span, an insertion point.
|
|
38
|
+
*
|
|
39
|
+
* Offsets are string indices, not bytes and not code points. Convert to a
|
|
40
|
+
* line and character with {@link LineIndex.spanOf}.
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
interface SourceRange {
|
|
45
|
+
/** The zero-based UTF-16 offset of the first character. */
|
|
46
|
+
readonly offset: number;
|
|
47
|
+
/** The number of UTF-16 code units in the span. */
|
|
48
|
+
readonly length: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Where something sits in a source text, said both ways: the
|
|
52
|
+
* {@link SourceRange} (`offset`, `length`) and the {@link SourcePosition} of
|
|
53
|
+
* its start (`line`, `character`, zero-based).
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* The positional core of every kit diagnostic. `JsoncParseErrorDetail`
|
|
57
|
+
* satisfies it, and so do `YamlDiagnostic`, `TomlDiagnostic`,
|
|
58
|
+
* `MarkdownDiagnostic` and `YamlToken`. {@link LineIndex.locationOf} builds
|
|
59
|
+
* one from a range.
|
|
60
|
+
*
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
interface SourceLocation extends SourceRange, SourcePosition {}
|
|
64
|
+
/**
|
|
65
|
+
* A diagnostic about a source text: a machine-readable `code`, a
|
|
66
|
+
* human-readable `message`, and its {@link SourceLocation}.
|
|
67
|
+
*
|
|
68
|
+
* @remarks
|
|
69
|
+
* A structural contract, not a base class. Each format package keeps its own
|
|
70
|
+
* diagnostic class, its own `code` union and its own error types, which stay
|
|
71
|
+
* part of that package's API; this interface only names the shape they
|
|
72
|
+
* already share, so a consumer (an LSP server, a CLI printing annotations, a
|
|
73
|
+
* lint host) can be written once over any of them. `YamlDiagnostic`,
|
|
74
|
+
* `TomlDiagnostic` and `MarkdownDiagnostic` satisfy it as they stand.
|
|
75
|
+
* `JsoncParseErrorDetail` carries no `message`, so it satisfies
|
|
76
|
+
* {@link SourceLocation} only.
|
|
77
|
+
*
|
|
78
|
+
* `Code` narrows the code union for a consumer that knows which package the
|
|
79
|
+
* diagnostic came from; the default accepts any.
|
|
80
|
+
*
|
|
81
|
+
* @typeParam Code - The diagnostic code union.
|
|
82
|
+
*
|
|
83
|
+
* @public
|
|
84
|
+
*/
|
|
85
|
+
interface SourceDiagnostic<Code extends string = string> extends SourceLocation {
|
|
86
|
+
/** The machine-readable diagnostic code. */
|
|
87
|
+
readonly code: Code;
|
|
88
|
+
/** The human-readable description. */
|
|
89
|
+
readonly message: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* A span of a source text as two {@link SourcePosition}s, the shape of an LSP
|
|
93
|
+
* `Range`. `end` is exclusive.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* The line-and-character twin of {@link SourceRange}. An LSP server reports
|
|
97
|
+
* and receives this shape; a parser reports offsets. {@link LineIndex.spanOf}
|
|
98
|
+
* and {@link LineIndex.rangeOf} convert between them.
|
|
99
|
+
*
|
|
100
|
+
* @public
|
|
101
|
+
*/
|
|
102
|
+
interface SourceSpan {
|
|
103
|
+
/** The position of the first character. */
|
|
104
|
+
readonly start: SourcePosition;
|
|
105
|
+
/** The position just past the last character. */
|
|
106
|
+
readonly end: SourcePosition;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The public surface of a line index, as a structural interface: what
|
|
110
|
+
* {@link LineIndex} answers, with nothing that ties a value to one copy of
|
|
111
|
+
* this package.
|
|
112
|
+
*
|
|
113
|
+
* @remarks
|
|
114
|
+
* Type a signature that takes or returns a line index with this interface,
|
|
115
|
+
* never with the `LineIndex` class. The class has `private` members, and
|
|
116
|
+
* TypeScript compares a class with `private` members nominally: when two
|
|
117
|
+
* copies of `@effected/source` are installed (a format package on one
|
|
118
|
+
* version, its consumer on another), a `LineIndex` built by one copy is not
|
|
119
|
+
* assignable to the other copy's `LineIndex` type (TS2345, "Types have
|
|
120
|
+
* separate declarations of a private property"). Every member here is a
|
|
121
|
+
* public method or a readonly property, so an index from any copy satisfies
|
|
122
|
+
* this interface from any other.
|
|
123
|
+
*
|
|
124
|
+
* Construct one with {@link LineIndex.make}; this interface has no
|
|
125
|
+
* constructor of its own, and nothing checks `instanceof`.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```ts
|
|
129
|
+
* import type { SourceIndex, SourcePosition } from "@effected/source";
|
|
130
|
+
* import { LineIndex } from "@effected/source";
|
|
131
|
+
*
|
|
132
|
+
* const startOf = (index: SourceIndex, offset: number): SourcePosition => index.positionAt(offset);
|
|
133
|
+
* console.log(startOf(LineIndex.make("a\nb"), 2));
|
|
134
|
+
* // => { line: 1, character: 0 }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @public
|
|
138
|
+
*/
|
|
139
|
+
interface SourceIndex {
|
|
140
|
+
/** The text this index was built over. */
|
|
141
|
+
readonly text: string;
|
|
142
|
+
/** The rule that decided where lines end. */
|
|
143
|
+
readonly lineBreaks: LineBreaks;
|
|
144
|
+
/** The number of lines, at least `1`. */
|
|
145
|
+
readonly lineCount: number;
|
|
146
|
+
/** The zero-based line that contains `offset`; see {@link LineIndex.lineAt}. */
|
|
147
|
+
lineAt(offset: number): number;
|
|
148
|
+
/** The offset of the first character of `line`; see {@link LineIndex.lineStart}. */
|
|
149
|
+
lineStart(line: number): number;
|
|
150
|
+
/** The offset where `line`'s terminator starts; see {@link LineIndex.lineEnd}. */
|
|
151
|
+
lineEnd(line: number): number;
|
|
152
|
+
/** The text of `line`, without its terminator; see {@link LineIndex.lineText}. */
|
|
153
|
+
lineText(line: number): string;
|
|
154
|
+
/** The `{ line, character }` of `offset`; see {@link LineIndex.positionAt}. */
|
|
155
|
+
positionAt(offset: number): SourcePosition;
|
|
156
|
+
/** The offset of a position; see {@link LineIndex.offsetAt}. */
|
|
157
|
+
offsetAt(position: SourcePosition): number;
|
|
158
|
+
/** The span of an offset range; see {@link LineIndex.spanOf}. */
|
|
159
|
+
spanOf(range: SourceRange): SourceSpan;
|
|
160
|
+
/** The offset range of a span; see {@link LineIndex.rangeOf}. */
|
|
161
|
+
rangeOf(span: SourceSpan): SourceRange;
|
|
162
|
+
/** The location of a range; see {@link LineIndex.locationOf}. */
|
|
163
|
+
locationOf(range: SourceRange): SourceLocation;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Which characters end a line, for {@link LineIndex.make}.
|
|
167
|
+
*
|
|
168
|
+
* @remarks
|
|
169
|
+
* - `"lsp"` (the default): `\n`, `\r\n` and a bare `\r`, the Language Server
|
|
170
|
+
* Protocol's rule. `\r\n` is one terminator. Markdown, TOML, YAML (its
|
|
171
|
+
* tokens, lint lines and diagnostics alike) and an LSP client all count
|
|
172
|
+
* lines this way.
|
|
173
|
+
* - `"lf"`: a line ends at `\n` only. `\r\n` is still one terminator (the
|
|
174
|
+
* `\r` is not part of the line's text), but a bare `\r` is an ordinary
|
|
175
|
+
* character. No kit package uses it now; it stays for a consumer that must
|
|
176
|
+
* reproduce a `\n`-only line count, such as a tool that splits on `\n`.
|
|
177
|
+
* - `"unicode"`: the `"lsp"` rule plus U+2028 LINE SEPARATOR and U+2029
|
|
178
|
+
* PARAGRAPH SEPARATOR, each a terminator on its own. `@effected/jsonc`
|
|
179
|
+
* counts its diagnostics' lines this way, as its scanner does. An LSP
|
|
180
|
+
* client does not, so a position computed under this rule disagrees with
|
|
181
|
+
* the client's after the first separator.
|
|
182
|
+
*
|
|
183
|
+
* @public
|
|
184
|
+
*/
|
|
185
|
+
type LineBreaks = "lsp" | "lf" | "unicode";
|
|
186
|
+
/**
|
|
187
|
+
* Options for {@link LineIndex.make}.
|
|
188
|
+
*
|
|
189
|
+
* @public
|
|
190
|
+
*/
|
|
191
|
+
interface LineIndexOptions {
|
|
192
|
+
/**
|
|
193
|
+
* Which characters end a line. Defaults to `"lsp"`: `\n`, `\r\n` and a
|
|
194
|
+
* bare `\r`.
|
|
195
|
+
*/
|
|
196
|
+
readonly lineBreaks?: LineBreaks;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* A line table over one source text, built once and queried many times:
|
|
200
|
+
* offset to `{ line, character }` and back, the number of lines, and where
|
|
201
|
+
* each line starts and ends.
|
|
202
|
+
*
|
|
203
|
+
* @remarks
|
|
204
|
+
* Positions are zero-based and `character` counts UTF-16 code units, LSP's
|
|
205
|
+
* default encoding (see {@link SourcePosition}). Building the index is one
|
|
206
|
+
* forward scan, `O(n)`; every lookup is a binary search, `O(log n)`, so build
|
|
207
|
+
* one per text and reuse it rather than rescanning for every diagnostic.
|
|
208
|
+
*
|
|
209
|
+
* Out-of-range input clamps instead of throwing, following
|
|
210
|
+
* `vscode-languageserver-textdocument`'s `TextDocument`, which most LSP
|
|
211
|
+
* servers already use:
|
|
212
|
+
*
|
|
213
|
+
* - an offset below `0` reads as `0`, and one past the end as `text.length`;
|
|
214
|
+
* - an offset inside a two-character terminator (the `\n` of a `\r\n`) reads
|
|
215
|
+
* as the end of its line, because no position can name the middle of a
|
|
216
|
+
* terminator;
|
|
217
|
+
* - a `character` past the end of its line reads as the end of the line's
|
|
218
|
+
* text, before its terminator, and a negative one as the line's start;
|
|
219
|
+
* - a `line` below `0` reads as offset `0`, and one past the last line as
|
|
220
|
+
* `text.length`.
|
|
221
|
+
*
|
|
222
|
+
* A value that is not an integer (`NaN`, `1.5`, `Infinity`) is a programmer
|
|
223
|
+
* error, not out-of-range input, and throws a `RangeError`: clamping `NaN`
|
|
224
|
+
* would silently answer "line 0".
|
|
225
|
+
*
|
|
226
|
+
* Every text has at least one line. The empty text has one empty line, and a
|
|
227
|
+
* text ending in a terminator has an empty last line after it, so `"a\n"` has
|
|
228
|
+
* two lines, as an editor shows it.
|
|
229
|
+
*
|
|
230
|
+
* `character` can name the low half of a surrogate pair; LSP leaves that to
|
|
231
|
+
* the client, and so does this index. It offers no code-point encoding:
|
|
232
|
+
* every LSP client must accept UTF-16, and each kit format package reports
|
|
233
|
+
* UTF-16 offsets.
|
|
234
|
+
*
|
|
235
|
+
* There is no shared-instance contract. Nothing checks `instanceof LineIndex`,
|
|
236
|
+
* and every value it returns is a plain record, so two installed copies of
|
|
237
|
+
* this package interoperate at runtime. At the type level the class does
|
|
238
|
+
* not: its `private` members make TypeScript compare it nominally, so a
|
|
239
|
+
* `LineIndex` from one copy is not assignable to another copy's `LineIndex`.
|
|
240
|
+
* Type a signature that takes or returns an index with the structural
|
|
241
|
+
* {@link SourceIndex} interface this class implements, never with the class.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { LineIndex } from "@effected/source";
|
|
246
|
+
*
|
|
247
|
+
* const index = LineIndex.make("name: kit\r\nversion: 1\n");
|
|
248
|
+
* console.log(index.lineCount);
|
|
249
|
+
* // => 3
|
|
250
|
+
* console.log(index.positionAt(11));
|
|
251
|
+
* // => { line: 1, character: 0 }
|
|
252
|
+
* console.log(index.offsetAt({ line: 1, character: 9 }));
|
|
253
|
+
* // => 20
|
|
254
|
+
* console.log(index.lineText(1));
|
|
255
|
+
* // => "version: 1"
|
|
256
|
+
* ```
|
|
257
|
+
*
|
|
258
|
+
* @public
|
|
259
|
+
*/
|
|
260
|
+
export declare class LineIndex implements SourceIndex {
|
|
261
|
+
/** The text this index was built over. */
|
|
262
|
+
readonly text: string;
|
|
263
|
+
/** The rule that decided where lines end. */
|
|
264
|
+
readonly lineBreaks: LineBreaks;
|
|
265
|
+
private readonly starts;
|
|
266
|
+
private constructor();
|
|
267
|
+
/**
|
|
268
|
+
* Build the line table for `text` with one forward scan.
|
|
269
|
+
*
|
|
270
|
+
* @param text - The source text.
|
|
271
|
+
* @param options - Which characters end a line; see {@link LineIndexOptions}.
|
|
272
|
+
* @returns The index.
|
|
273
|
+
*/
|
|
274
|
+
static make(text: string, options?: LineIndexOptions): LineIndex;
|
|
275
|
+
/** The number of lines, at least `1`. */
|
|
276
|
+
get lineCount(): number;
|
|
277
|
+
/**
|
|
278
|
+
* The zero-based line that contains `offset`. An offset at a terminator
|
|
279
|
+
* belongs to the line the terminator ends.
|
|
280
|
+
*
|
|
281
|
+
* @param offset - A UTF-16 offset, clamped to `[0, text.length]`.
|
|
282
|
+
* @returns The line number.
|
|
283
|
+
*/
|
|
284
|
+
lineAt(offset: number): number;
|
|
285
|
+
/**
|
|
286
|
+
* The offset of the first character of `line`.
|
|
287
|
+
*
|
|
288
|
+
* @param line - A zero-based line, clamped to `[0, lineCount - 1]`.
|
|
289
|
+
* @returns The line's start offset.
|
|
290
|
+
*/
|
|
291
|
+
lineStart(line: number): number;
|
|
292
|
+
/**
|
|
293
|
+
* The offset just past the last character of `line`'s text, which is
|
|
294
|
+
* where its terminator starts, or `text.length` on the last line.
|
|
295
|
+
*
|
|
296
|
+
* @param line - A zero-based line, clamped to `[0, lineCount - 1]`.
|
|
297
|
+
* @returns The line's end offset, excluding its terminator.
|
|
298
|
+
*/
|
|
299
|
+
lineEnd(line: number): number;
|
|
300
|
+
/**
|
|
301
|
+
* The text of `line`, without its terminator.
|
|
302
|
+
*
|
|
303
|
+
* @param line - A zero-based line, clamped to `[0, lineCount - 1]`.
|
|
304
|
+
* @returns The line's text.
|
|
305
|
+
*/
|
|
306
|
+
lineText(line: number): string;
|
|
307
|
+
/**
|
|
308
|
+
* The zero-based `{ line, character }` of `offset`.
|
|
309
|
+
*
|
|
310
|
+
* @param offset - A UTF-16 offset, clamped to `[0, text.length]`; one
|
|
311
|
+
* inside a `\r\n` reads as the end of its line.
|
|
312
|
+
* @returns The position.
|
|
313
|
+
*/
|
|
314
|
+
positionAt(offset: number): SourcePosition;
|
|
315
|
+
/**
|
|
316
|
+
* The offset of a zero-based `{ line, character }`.
|
|
317
|
+
*
|
|
318
|
+
* @param position - The position. A `line` past the last reads as
|
|
319
|
+
* `text.length` and one below `0` as `0`; a `character` past the line's
|
|
320
|
+
* text reads as the line's end, before its terminator.
|
|
321
|
+
* @returns The UTF-16 offset.
|
|
322
|
+
*/
|
|
323
|
+
offsetAt(position: SourcePosition): number;
|
|
324
|
+
/**
|
|
325
|
+
* The {@link SourceSpan} (LSP `Range`) of an offset range.
|
|
326
|
+
*
|
|
327
|
+
* @param range - The range; its `length` must not be negative.
|
|
328
|
+
* @returns The start and exclusive end positions.
|
|
329
|
+
*/
|
|
330
|
+
spanOf(range: SourceRange): SourceSpan;
|
|
331
|
+
/**
|
|
332
|
+
* The {@link SourceRange} of a {@link SourceSpan}, after each position is
|
|
333
|
+
* clamped as {@link LineIndex.offsetAt} clamps it.
|
|
334
|
+
*
|
|
335
|
+
* @param span - The span; `end` must not precede `start`.
|
|
336
|
+
* @returns The offset and length.
|
|
337
|
+
*/
|
|
338
|
+
rangeOf(span: SourceSpan): SourceRange;
|
|
339
|
+
/**
|
|
340
|
+
* The {@link SourceLocation} of a range: the range itself, unchanged,
|
|
341
|
+
* with the position of its start. The shape a diagnostic carries.
|
|
342
|
+
*
|
|
343
|
+
* @param range - The range; its `length` must be a non-negative integer,
|
|
344
|
+
* as {@link LineIndex.spanOf} requires.
|
|
345
|
+
* @returns `offset`, `length`, `line` and `character`.
|
|
346
|
+
*/
|
|
347
|
+
locationOf(range: SourceRange): SourceLocation;
|
|
348
|
+
/**
|
|
349
|
+
* Where `line`'s text ends, derived from the next line's start rather
|
|
350
|
+
* than stored: the next start minus the terminator's length, two for a
|
|
351
|
+
* `\r\n` and one for every other terminator, or `text.length` on the
|
|
352
|
+
* last line. Keeping one table halves the index's memory; the lookup
|
|
353
|
+
* stays `O(1)` after the `O(log n)` search that found the line.
|
|
354
|
+
*/
|
|
355
|
+
private endOf;
|
|
356
|
+
private clampLine;
|
|
357
|
+
}
|
|
358
|
+
export type { LineBreaks, LineIndexOptions, SourceDiagnostic, SourceIndex, SourceLocation, SourcePosition, SourceRange, SourceSpan };
|
|
359
|
+
//# sourceMappingURL=index.d.ts.map
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@effected/source",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Source-text positions as plain values: a line index over a text, LSP-style line and character, and the structural range and diagnostic shapes the kit's format packages share.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"source",
|
|
8
|
+
"position",
|
|
9
|
+
"line",
|
|
10
|
+
"character",
|
|
11
|
+
"offset",
|
|
12
|
+
"lsp",
|
|
13
|
+
"diagnostic",
|
|
14
|
+
"range",
|
|
15
|
+
"effected"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/spencerbeggs/effected/tree/main/packages/source#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/spencerbeggs/effected/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/spencerbeggs/effected.git",
|
|
24
|
+
"directory": "packages/source"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "C. Spencer Beggs",
|
|
29
|
+
"email": "spencer@beggs.codes",
|
|
30
|
+
"url": "https://spencerbeg.gs"
|
|
31
|
+
},
|
|
32
|
+
"sideEffects": false,
|
|
33
|
+
"type": "module",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./index.d.ts",
|
|
37
|
+
"import": "./index.js",
|
|
38
|
+
"default": "./index.js"
|
|
39
|
+
},
|
|
40
|
+
"./package.json": "./package.json"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=24.11.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// This file is read by tools that parse documentation comments conforming to the TSDoc standard.
|
|
2
|
+
// It should be published with your NPM package. It should not be tracked by Git.
|
|
3
|
+
{
|
|
4
|
+
"tsdocVersion": "0.12",
|
|
5
|
+
"toolPackages": [
|
|
6
|
+
{
|
|
7
|
+
"packageName": "@microsoft/api-extractor",
|
|
8
|
+
"packageVersion": "7.59.4"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|