@remnic/coding-graph 9.3.759

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 (86) hide show
  1. package/README.md +130 -0
  2. package/dist/chunk-5I2DBHOQ.js +1042 -0
  3. package/dist/chunk-5I2DBHOQ.js.map +1 -0
  4. package/dist/chunk-CPYJACC5.js +1838 -0
  5. package/dist/chunk-CPYJACC5.js.map +1 -0
  6. package/dist/chunk-ZVCMIM4T.js +216 -0
  7. package/dist/chunk-ZVCMIM4T.js.map +1 -0
  8. package/dist/cypher/query-parser.d.ts +253 -0
  9. package/dist/cypher/query-parser.js +17 -0
  10. package/dist/cypher/query-parser.js.map +1 -0
  11. package/dist/graph-schema.d.ts +84 -0
  12. package/dist/graph-schema.js +17 -0
  13. package/dist/graph-schema.js.map +1 -0
  14. package/dist/graph-store.d.ts +938 -0
  15. package/dist/graph-store.js +16 -0
  16. package/dist/graph-store.js.map +1 -0
  17. package/dist/index.d.ts +1953 -0
  18. package/dist/index.js +3509 -0
  19. package/dist/index.js.map +1 -0
  20. package/grammars/tree-sitter-bash.wasm +0 -0
  21. package/grammars/tree-sitter-c.wasm +0 -0
  22. package/grammars/tree-sitter-c_sharp.wasm +0 -0
  23. package/grammars/tree-sitter-cpp.wasm +0 -0
  24. package/grammars/tree-sitter-go.wasm +0 -0
  25. package/grammars/tree-sitter-java.wasm +0 -0
  26. package/grammars/tree-sitter-javascript.wasm +0 -0
  27. package/grammars/tree-sitter-kotlin.wasm +0 -0
  28. package/grammars/tree-sitter-php.wasm +0 -0
  29. package/grammars/tree-sitter-python.wasm +0 -0
  30. package/grammars/tree-sitter-ruby.wasm +0 -0
  31. package/grammars/tree-sitter-rust.wasm +0 -0
  32. package/grammars/tree-sitter-swift.wasm +0 -0
  33. package/grammars/tree-sitter-tsx.wasm +0 -0
  34. package/grammars/tree-sitter-typescript.wasm +0 -0
  35. package/package.json +79 -0
  36. package/src/co-change.test.ts +175 -0
  37. package/src/co-change.ts +167 -0
  38. package/src/cypher/query-parser.test.ts +1107 -0
  39. package/src/cypher/query-parser.ts +1692 -0
  40. package/src/detect-changes.test.ts +533 -0
  41. package/src/detect-changes.ts +367 -0
  42. package/src/engine/emit.ts +556 -0
  43. package/src/engine/engine.test.ts +1417 -0
  44. package/src/engine/engine.ts +182 -0
  45. package/src/engine/extractors.ts +486 -0
  46. package/src/engine/fixtures.ts +364 -0
  47. package/src/engine/language-sniff.ts +56 -0
  48. package/src/engine/parser-backend.ts +206 -0
  49. package/src/engine/utf16-offsets.ts +68 -0
  50. package/src/git-invoker.test.ts +116 -0
  51. package/src/git-invoker.ts +426 -0
  52. package/src/graph-schema.test.ts +541 -0
  53. package/src/graph-schema.ts +383 -0
  54. package/src/graph-store-pr2.test.ts +1879 -0
  55. package/src/graph-store.test.ts +1420 -0
  56. package/src/graph-store.ts +3489 -0
  57. package/src/index-status.test.ts +303 -0
  58. package/src/index-status.ts +135 -0
  59. package/src/index.ts +384 -0
  60. package/src/lsp/byte-position.ts +173 -0
  61. package/src/lsp/characterization.test.ts +174 -0
  62. package/src/lsp/client.test.ts +275 -0
  63. package/src/lsp/client.ts +484 -0
  64. package/src/lsp/config.ts +219 -0
  65. package/src/lsp/degradation.ts +86 -0
  66. package/src/lsp/fixtures/fake-server.mjs +198 -0
  67. package/src/lsp/framing.test.ts +180 -0
  68. package/src/lsp/framing.ts +177 -0
  69. package/src/lsp/resolution.test.ts +497 -0
  70. package/src/lsp/resolution.ts +483 -0
  71. package/src/lsp/status.ts +140 -0
  72. package/src/lsp/types.ts +167 -0
  73. package/src/reindex.test.ts +1038 -0
  74. package/src/reindex.ts +908 -0
  75. package/src/row-types.ts +45 -0
  76. package/src/semantic/canonical-text.test.ts +150 -0
  77. package/src/semantic/canonical-text.ts +219 -0
  78. package/src/semantic/config.ts +235 -0
  79. package/src/semantic/index.ts +78 -0
  80. package/src/semantic/minhash.test.ts +197 -0
  81. package/src/semantic/minhash.ts +261 -0
  82. package/src/semantic/semantic-query.ts +173 -0
  83. package/src/semantic/semantic.test.ts +1315 -0
  84. package/src/semantic/similarity.ts +268 -0
  85. package/src/semantic/types.ts +145 -0
  86. package/src/semantic/vectors.ts +235 -0
package/src/index.ts ADDED
@@ -0,0 +1,384 @@
1
+ /**
2
+ * @remnic/coding-graph — symbol-extraction engine + SQLite knowledge-graph
3
+ * store for codebase memory.
4
+ *
5
+ * À-la-carte optional companion of @remnic/core (CLAUDE.md rule 57).
6
+ *
7
+ * This package unifies two PR1 surfaces:
8
+ * - The web-tree-sitter engine scaffold (#1551 step 1): the package and
9
+ * its build wiring exist; the engine public surface is declared and
10
+ * the placeholder factory throws a tagged
11
+ * `CodingGraphError("not_implemented", …)`. The real backend lands in
12
+ * #1551 PR2.
13
+ * - The SQLite knowledge-graph store (#1552 PR1): versioned schema + the
14
+ * write pipeline (upsert/drop file batches, node-id derivation,
15
+ * dangling-edge accounting). Traversal, search, dead-code, and the
16
+ * openCypher subset land in #1552 PR2/PR3.
17
+ *
18
+ * Type-source direction:
19
+ * The contract types (CodingGraphEngine, FileIR, etc.) and the
20
+ * TIER_1_LANGUAGES / CODING_GRAPH_ENGINE_VERSION constants live in
21
+ * @remnic/core (packages/remnic-core/src/coding/coding-graph-types.ts,
22
+ * re-exported from the main index). This package imports them and
23
+ * implements against them; it does NOT redefine them. That keeps a
24
+ * single source of truth so updating the engine version in one place
25
+ * keeps every consumer in lockstep (Cursor Bugbot low-severity on
26
+ * PR #1588 round 2: "ENGINE_VERSION duplicated not imported").
27
+ *
28
+ * @remnic/coding-graph declares @remnic/core as both `peerDependencies`
29
+ * and `devDependencies: "workspace:*"` in its package.json, so the
30
+ * pnpm workspace link exists and the `import from "@remnic/core"`
31
+ * below resolves in development.
32
+ *
33
+ * The store modules (graph-schema, graph-store, row-types) are local to
34
+ * this package; they import `openBetterSqlite3` from
35
+ * `@remnic/core/runtime/better-sqlite` so the native-binding lifecycle
36
+ * is paid for once there (rule 23/38: do not invent a new pattern).
37
+ *
38
+ * IR-type re-export policy:
39
+ * graph-store.ts imports the core IR contract types (`FileIR`,
40
+ * `SymbolIR`, etc.) from `@remnic/core/coding/coding-graph-types`
41
+ * and re-exports them so existing `import { type FileIR } from
42
+ * "./graph-store.js"` call-sites continue to resolve. The store
43
+ * does NOT redefine these types — it derives from the core contract
44
+ * so PR2 callers can pass `ParseResult.ir` directly
45
+ * (chatgpt-codex-connector P2: 'Derive store FileIR from the core
46
+ * parser contract'). At the package root, `FileIR`/`SymbolIR`
47
+ * resolve to the @remnic/core contract types re-exported below;
48
+ * the store-specific `StoreFileIR` (FileIR + edges extension) and
49
+ * `EdgeIR` are re-exported from the root via graph-store.
50
+ */
51
+
52
+ import {
53
+ CODING_GRAPH_ENGINE_VERSION,
54
+ TIER_1_LANGUAGES,
55
+ type CodingGraphEngine,
56
+ type CodingGraphErrorCode,
57
+ type CodingGraphLanguage,
58
+ type CreateCodingGraphEngineOptions,
59
+ type FileIR,
60
+ type ParseFileInput,
61
+ type ParseResult,
62
+ type SymbolIR,
63
+ } from "@remnic/core";
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Engine version — single source of truth lives in @remnic/core as
67
+ // `CODING_GRAPH_ENGINE_VERSION`. We re-export it under the conventional
68
+ // `ENGINE_VERSION` name (the dynamic-import loader in @remnic/core
69
+ // validates the shape using this field name) AND keep the core alias
70
+ // available so any consumer that wants the local name or the core
71
+ // name gets the same value. Updating the constant in core propagates
72
+ // here automatically.
73
+ // ---------------------------------------------------------------------------
74
+
75
+ /** Public engine version. Imported from @remnic/core (single source of truth). */
76
+ export const ENGINE_VERSION = CODING_GRAPH_ENGINE_VERSION;
77
+
78
+ /** Core-alias re-export so callers can use either name. */
79
+ export { CODING_GRAPH_ENGINE_VERSION };
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Tier-1 language list re-export. The list itself lives in @remnic/core
83
+ // (single source of truth) so consumers that reach the optional package
84
+ // via the loader get the same shape they would get from core directly.
85
+ // ---------------------------------------------------------------------------
86
+ export { TIER_1_LANGUAGES };
87
+ export type { CodingGraphLanguage };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Tagged error — `code` is the load-bearing signal for programmatic
91
+ // detection (see PR2 contract).
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export type { CodingGraphErrorCode } from "@remnic/core";
95
+
96
+ /**
97
+ * Thrown by `createCodingGraphEngine` while the real implementation is
98
+ * being landed. It is *not* a generic Error — the `code` field is the
99
+ * load-bearing signal for programmatic detection (see PR2 contract).
100
+ */
101
+ export class CodingGraphError extends Error {
102
+ readonly code: CodingGraphErrorCode;
103
+ readonly engineVersion: string;
104
+
105
+ constructor(
106
+ code: CodingGraphErrorCode,
107
+ message: string,
108
+ engineVersion: string = ENGINE_VERSION,
109
+ ) {
110
+ super(message);
111
+ this.name = "CodingGraphError";
112
+ this.code = code;
113
+ this.engineVersion = engineVersion;
114
+ }
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Engine implementation (PR2): WASM tree-sitter parser + tier-1 extractors.
119
+ // Delegates to ./engine/engine.js which wires the ParserBackend, per-language
120
+ // .scm queries, and the FileIR emitter.
121
+ // ---------------------------------------------------------------------------
122
+
123
+ export { createCodingGraphEngine } from "./engine/engine.js";
124
+
125
+ // Backwards-compat: re-export the engine module's internal types for any
126
+ // consumer that reached into the implementation.
127
+ export type { ParserBackend } from "./engine/parser-backend.js";
128
+ export { WasmTreeSitterBackend } from "./engine/parser-backend.js";
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Re-export the contract types so the public surface is stable whether a
132
+ // consumer reaches into @remnic/coding-graph or @remnic/core. `export type`
133
+ // guarantees these are erased at runtime — no double-emit.
134
+ // ---------------------------------------------------------------------------
135
+
136
+ export type {
137
+ CodingGraphEngine,
138
+ CreateCodingGraphEngineOptions,
139
+ FileIR,
140
+ ParseFileInput,
141
+ ParseResult,
142
+ SymbolIR,
143
+ };
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // SQLite knowledge-graph store (#1552 PR1 + PR2): versioned schema, write
147
+ // pipeline, and PR2 read primitives (traverse / searchGraph / schemaStats /
148
+ // deadCode / snippetFor). Re-exported from the package root so consumers
149
+ // can import `import { GraphStore } from "@remnic/coding-graph"` (the
150
+ // subpath exports `./graph-schema` and `./graph-store` remain available
151
+ // for callers that want only one half). Only non-colliding store types
152
+ // are re-exported here — see the file header's IR-type policy note.
153
+ // ---------------------------------------------------------------------------
154
+
155
+ export {
156
+ CODING_GRAPH_SCHEMA_VERSION,
157
+ EDGE_PROVENANCE_VALUES,
158
+ applyCodingGraphSchema,
159
+ readSchemaVersion,
160
+ isEdgeProvenance,
161
+ type EdgeProvenance,
162
+ } from "./graph-schema.js";
163
+
164
+ export {
165
+ GraphStore,
166
+ nodeIdFor,
167
+ DEFAULT_TRAVERSE_PATHS_MAX,
168
+ MAX_TRAVERSE_PATHS_HOPS,
169
+ DEAD_CODE_EXCLUSION,
170
+ type ByteSpan,
171
+ type DeadCodeHit,
172
+ type DeadCodeResult,
173
+ type EdgeIR,
174
+ type ExportIR,
175
+ type GraphStoreFailure,
176
+ type GraphStoreFailureCode,
177
+ type GraphStoreOptions,
178
+ type ImportIR,
179
+ type NodeIdInput,
180
+ type SchemaStats,
181
+ type SchemaStatsResult,
182
+ type SearchHit,
183
+ type SearchQuery,
184
+ type SearchResult,
185
+ type SnippetFailureCode,
186
+ type SnippetQuery,
187
+ type SnippetResult,
188
+ type SnippetSuccess,
189
+ type StoreFileIR,
190
+ type SymbolKind,
191
+ type TraverseDirection,
192
+ type TraverseHit,
193
+ type TraverseQuery,
194
+ type TraversePathHit,
195
+ type TraversePathsQuery,
196
+ type TraversePathsResult,
197
+ type TraverseResult,
198
+ type ReadCoChangeEdge,
199
+ type ReadCoChangesResult,
200
+ type ReadFileHashesResult,
201
+ type ReadMetaResult,
202
+ type UpsertBatchResult,
203
+ type UpsertEdgesResult,
204
+ type UpsertEdgesSuccess,
205
+ type UpsertResult,
206
+ type UpsertSuccess,
207
+ } from "./graph-store.js";
208
+ // ---------------------------------------------------------------------------
209
+ // PR3 (issue #1553): incremental git-based reindex, detect_changes + blast
210
+ // radius, co-change mining, and index-status reporting. Re-exported from
211
+ // the package root so consumers can import them alongside the store.
212
+ // ---------------------------------------------------------------------------
213
+
214
+ export {
215
+ planReindex,
216
+ executeReindex,
217
+ readLastIndexedHead,
218
+ readFileHashes,
219
+ hashContent,
220
+ META_KEY_LAST_HEAD,
221
+ META_KEY_PENDING_PARSE_FAILURES,
222
+ type ParseFileFn,
223
+ type ReadFileFn,
224
+ type ReindexGitFacts,
225
+ type ReindexPlan,
226
+ type ReindexResult,
227
+ type ReindexState,
228
+ } from "./reindex.js";
229
+
230
+ export {
231
+ classifyRisk,
232
+ computeBlastRadius,
233
+ findDirectlyAffectedSymbols,
234
+ byteSpanToLines,
235
+ rangesOverlap,
236
+ BLAST_RADIUS_EDGE_TYPES,
237
+ FAN_IN_ESCALATION_THRESHOLD,
238
+ DEFAULT_BLAST_RADIUS_DEPTH,
239
+ type AffectedSymbol,
240
+ type BlastRadiusResult,
241
+ type DetectChangesResult,
242
+ type RiskLevel,
243
+ } from "./detect-changes.js";
244
+
245
+ export {
246
+ mineCoChangeEdges,
247
+ mineAndStoreCoChanges,
248
+ DEFAULT_CO_CHANGE_CONFIG,
249
+ type CoChangeConfig,
250
+ type CoChangeEdge,
251
+ type MineCoChangesResult,
252
+ } from "./co-change.js";
253
+
254
+ export {
255
+ getIndexStatus,
256
+ type IndexStatus,
257
+ type IndexStatusMode,
258
+ } from "./index-status.js";
259
+
260
+ export {
261
+ defaultCodingGitInvoker,
262
+ parseNameStatus,
263
+ parseHunks,
264
+ parseLogFiles,
265
+ type CodingGitInvoker,
266
+ type DiffHunk,
267
+ type GitFailure,
268
+ type LogFilesEntry,
269
+ type NameStatusEntry,
270
+ } from "./git-invoker.js";
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // PR3 (issue #1552): openCypher read-subset — parser + executor. Re-exported
274
+ // from the package root so consumers can `import { executeCypher } from
275
+ // "@remnic/coding-graph"` (the subpath export `./cypher` remains for callers
276
+ // that want only this layer).
277
+ // ---------------------------------------------------------------------------
278
+
279
+ export {
280
+ executeCypher,
281
+ executeAst,
282
+ parseCypher,
283
+ VALID_CYPHER_LABELS,
284
+ type CypherAst,
285
+ type CypherFailure,
286
+ type CypherFailureCode,
287
+ type CypherNodeValue,
288
+ type CypherParseResult,
289
+ type CypherResult,
290
+ type CypherRow,
291
+ type CypherScalar,
292
+ type CypherSuccess,
293
+ type CypherValue,
294
+ } from "./cypher/query-parser.js";
295
+ // ---------------------------------------------------------------------------
296
+ // PR (issue #1555): Phase B type resolution — LSP client layer over
297
+ // installed language servers. Minimal JSON-RPC-over-stdio client, server
298
+ // registry, resolution pass, and status surfacing. All behind
299
+ // `codingGraph.lsp.enabled` (default false — rule 30/48).
300
+ // ---------------------------------------------------------------------------
301
+
302
+ export {
303
+ LspClient,
304
+ pathToUri,
305
+ uriToPath,
306
+ type LspClientOptions,
307
+ } from "./lsp/client.js";
308
+
309
+ export {
310
+ LspFrameDecoder,
311
+ encodeLspFrame,
312
+ type FrameDecodeError,
313
+ type FrameDecodeErrorKind,
314
+ type FrameDecodeResult,
315
+ } from "./lsp/framing.js";
316
+
317
+ export {
318
+ DEFAULT_LSP_CONFIG,
319
+ DEFAULT_LSP_MAX_REQUESTS_PER_RUN,
320
+ DEFAULT_LSP_TIMEOUT_MS,
321
+ parseLspConfig,
322
+ readLspEnabledEnv,
323
+ type LspConfig,
324
+ type LspConfigParseResult,
325
+ type LspServerLaunchSpec,
326
+ type LspServerOverrides,
327
+ } from "./lsp/config.js";
328
+
329
+ export {
330
+ lspDegradation,
331
+ type LspBackend,
332
+ type LspDegradation,
333
+ type LspDegradationCode,
334
+ type LspResult,
335
+ } from "./lsp/degradation.js";
336
+
337
+ export {
338
+ executeLspResolution,
339
+ mapLocationToNode,
340
+ planLspUpgrades,
341
+ type EdgeUpgrade,
342
+ type LspBudget,
343
+ type NodeLocator,
344
+ type PlanResult,
345
+ type PlannedLspRequest,
346
+ type ResolutionResult,
347
+ type ResolveOptions,
348
+ type UnresolvedCallSite,
349
+ } from "./lsp/resolution.js";
350
+
351
+ export {
352
+ formatLspStatusLine,
353
+ getLspStatus,
354
+ resolutionResultToStatusMaps,
355
+ type LspStatusEntry,
356
+ type LspStatusInput,
357
+ } from "./lsp/status.js";
358
+ export {
359
+ buildLineOffsetMap,
360
+ byteOffsetToPosition,
361
+ positionToByteOffset,
362
+ type LineOffsetMap,
363
+ } from "./lsp/byte-position.js";
364
+
365
+ export type {
366
+ LspInitializeParams,
367
+ LspInitializeResult,
368
+ LspLocation,
369
+ LspPosition,
370
+ LspRange,
371
+ LspServerCapabilities,
372
+ LspTextDocumentItem,
373
+ LspTextDocumentPositionParams,
374
+ } from "./lsp/types.js";
375
+
376
+
377
+ // ---------------------------------------------------------------------------
378
+ // Semantic layer (issue #1556): symbol embeddings, SIMILAR_TO near-clone
379
+ // edges, and semantic_query. OFF by default (SemanticConfig.enabled = false).
380
+ // The layer builds on @remnic/core's host embedding provider + fallback —
381
+ // no new embedding stack. See ./semantic/index.ts for the full surface.
382
+ // ---------------------------------------------------------------------------
383
+
384
+ export * from "./semantic/index.js";
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Byte-offset ↔ LSP position conversion.
3
+ *
4
+ * LSP positions are zero-based {line, character} where `character` is a
5
+ * UTF-16 code-unit offset within the line (LSP 3.17 §3.17). The coding-
6
+ * graph store uses UTF-8 byte spans. This module converts between the two.
7
+ *
8
+ * A {@link LineOffsetMap} pre-computes the UTF-8 BYTE offset of each line
9
+ * start, making both directions O(log n) via binary search for the line,
10
+ * then O(line length) for the character within the line.
11
+ */
12
+
13
+ /**
14
+ * Pre-computed line-start byte offsets for a single file. Built once per
15
+ * file from its content; reused for all position conversions in that file.
16
+ *
17
+ * `lineStarts[i]` = UTF-8 byte offset of the first character on line `i`.
18
+ * Line 0 always starts at byte 0.
19
+ */
20
+ export interface LineOffsetMap {
21
+ readonly lineStarts: readonly number[];
22
+ }
23
+
24
+ /**
25
+ * Compute the UTF-8 byte length of a single UTF-16 code unit or a
26
+ * surrogate pair. Surrogate pairs encode to 4 bytes (U+10000-U+10FFFF).
27
+ */
28
+ function utf8ByteLength(code: number, nextCode: number): number {
29
+ if (code >= 0xd800 && code <= 0xdbff && nextCode >= 0xdc00 && nextCode <= 0xdfff) {
30
+ return 4;
31
+ }
32
+ if (code < 0x80) return 1;
33
+ if (code < 0x800) return 2;
34
+ return 3;
35
+ }
36
+
37
+ /** True if this code unit is the high half of a surrogate pair. */
38
+ function isHighSurrogate(code: number, nextCode: number): boolean {
39
+ return code >= 0xd800 && code <= 0xdbff && nextCode >= 0xdc00 && nextCode <= 0xdfff;
40
+ }
41
+
42
+ /**
43
+ * Build a line-offset map from file content (as a UTF-8 string or Buffer).
44
+ * Records UTF-8 BYTE offsets — not UTF-16 string indices — because
45
+ * Content-Length and the store's span_start/span_end count bytes.
46
+ * Handles `\n`, `\r\n`, and `\r` line endings.
47
+ */
48
+ export function buildLineOffsetMap(content: string | Buffer): LineOffsetMap {
49
+ const text = typeof content === "string" ? content : content.toString("utf8");
50
+ const lineStarts = [0];
51
+ let byteOffset = 0;
52
+
53
+ for (let i = 0; i < text.length; i++) {
54
+ const code = text.charCodeAt(i);
55
+ const nextCode = i + 1 < text.length ? text.charCodeAt(i + 1) : 0;
56
+
57
+ if (code === 0x0d && nextCode === 0x0a) {
58
+ // \r\n — 2 bytes, advance past \n.
59
+ byteOffset += 2;
60
+ lineStarts.push(byteOffset);
61
+ i++;
62
+ } else if (code === 0x0a || code === 0x0d) {
63
+ byteOffset += 1;
64
+ lineStarts.push(byteOffset);
65
+ } else {
66
+ byteOffset += utf8ByteLength(code, nextCode);
67
+ if (isHighSurrogate(code, nextCode)) i++;
68
+ }
69
+ }
70
+ return { lineStarts };
71
+ }
72
+
73
+ /**
74
+ * Convert a UTF-8 byte offset to an LSP position {line, character}.
75
+ * `character` is a UTF-16 code-unit count from the line start (surrogates
76
+ * count as 2, matching LSP §3.17).
77
+ */
78
+ export function byteOffsetToPosition(
79
+ content: string,
80
+ byteOffset: number,
81
+ map: LineOffsetMap,
82
+ ): { line: number; character: number } {
83
+ const lineIdx = binarySearchLine(map.lineStarts, byteOffset);
84
+ const lineByteStart = map.lineStarts[lineIdx];
85
+
86
+ // Find the string index where the line starts (byte offset == lineByteStart).
87
+ let strIdx = 0;
88
+ let byteAccum = 0;
89
+ for (strIdx = 0; strIdx < content.length; strIdx++) {
90
+ if (byteAccum >= lineByteStart) break;
91
+ const code = content.charCodeAt(strIdx);
92
+ const nextCode = strIdx + 1 < content.length ? content.charCodeAt(strIdx + 1) : 0;
93
+ byteAccum += utf8ByteLength(code, nextCode);
94
+ if (isHighSurrogate(code, nextCode)) strIdx++;
95
+ }
96
+
97
+ // Walk from the line start to byteOffset, counting UTF-16 code units.
98
+ let charCount = 0;
99
+ let currentByte = lineByteStart;
100
+ for (let i = strIdx; i < content.length; i++) {
101
+ if (currentByte >= byteOffset) break;
102
+ const code = content.charCodeAt(i);
103
+ const nextCode = i + 1 < content.length ? content.charCodeAt(i + 1) : 0;
104
+ currentByte += utf8ByteLength(code, nextCode);
105
+ if (isHighSurrogate(code, nextCode)) {
106
+ charCount += 2;
107
+ i++;
108
+ } else {
109
+ charCount++;
110
+ }
111
+ }
112
+
113
+ return { line: lineIdx, character: charCount };
114
+ }
115
+
116
+ /**
117
+ * Convert an LSP position {line, character} to a UTF-8 byte offset.
118
+ * `character` is a UTF-16 code-unit count from the line start.
119
+ */
120
+ export function positionToByteOffset(
121
+ content: string,
122
+ position: { line: number; character: number },
123
+ map: LineOffsetMap,
124
+ ): number {
125
+ const lineIdx = Math.min(position.line, map.lineStarts.length - 1);
126
+ const lineByteStart = map.lineStarts[lineIdx];
127
+
128
+ // Find the string index where the line starts (byte offset == lineByteStart).
129
+ let strIdx = 0;
130
+ let byteAccum = 0;
131
+ for (strIdx = 0; strIdx < content.length; strIdx++) {
132
+ if (byteAccum >= lineByteStart) break;
133
+ const code = content.charCodeAt(strIdx);
134
+ const nextCode = strIdx + 1 < content.length ? content.charCodeAt(strIdx + 1) : 0;
135
+ byteAccum += utf8ByteLength(code, nextCode);
136
+ if (isHighSurrogate(code, nextCode)) strIdx++;
137
+ }
138
+
139
+ // Walk from the line start, counting UTF-16 code units until position.character.
140
+ let charCount = 0;
141
+ let byteOffset = lineByteStart;
142
+ for (let i = strIdx; i < content.length && charCount < position.character; i++) {
143
+ const code = content.charCodeAt(i);
144
+ const nextCode = i + 1 < content.length ? content.charCodeAt(i + 1) : 0;
145
+ byteOffset += utf8ByteLength(code, nextCode);
146
+ if (isHighSurrogate(code, nextCode)) {
147
+ charCount += 2;
148
+ i++;
149
+ } else {
150
+ charCount++;
151
+ }
152
+ }
153
+
154
+ return byteOffset;
155
+ }
156
+
157
+ /**
158
+ * Binary search for the last lineStart that is <= byteOffset.
159
+ * Returns the line index (0-based).
160
+ */
161
+ function binarySearchLine(lineStarts: readonly number[], byteOffset: number): number {
162
+ let lo = 0;
163
+ let hi = lineStarts.length - 1;
164
+ while (lo < hi) {
165
+ const mid = (lo + hi + 1) >> 1;
166
+ if (lineStarts[mid] <= byteOffset) {
167
+ lo = mid;
168
+ } else {
169
+ hi = mid - 1;
170
+ }
171
+ }
172
+ return lo;
173
+ }