@pieceful/ravel-language-service 0.2.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.
@@ -0,0 +1,30 @@
1
+ import type {
2
+ Diagnostic,
3
+ PretransformGraph,
4
+ RavelProgram,
5
+ SourceLocation,
6
+ SourcePosition
7
+ } from "@pieceful/ravel-core";
8
+
9
+ export interface RavelSemanticIndex {
10
+ readonly revision?: string;
11
+ readonly chunks: readonly RavelProgram["chunks"][string][];
12
+ readonly references: readonly Record<string, unknown>[];
13
+ readonly directives: readonly Record<string, unknown>[];
14
+ readonly diagnostics: readonly Diagnostic[];
15
+ readonly symbols: readonly Record<string, unknown>[];
16
+ entityAt(uri: string, position: SourcePosition): Record<string, unknown> | null;
17
+ definitionAt(uri: string, position: SourcePosition): (SourceLocation & Record<string, unknown>) | null;
18
+ referencesFor(id: string, options?: { includeDeclaration?: boolean }): Array<SourceLocation & Record<string, unknown>>;
19
+ hoverAt(uri: string, position: SourcePosition): Record<string, unknown> | null;
20
+ completeReferences(query?: string, options?: { documentId?: string }): Array<Record<string, unknown>>;
21
+ documentSymbols(uri: string): Array<Record<string, unknown>>;
22
+ workspaceSymbols(query?: string): Array<Record<string, unknown>>;
23
+ diagnosticsFor(uri: string): Diagnostic[];
24
+ }
25
+
26
+ export function createRavelSemanticIndex(context: {
27
+ program: RavelProgram;
28
+ pretransform?: PretransformGraph;
29
+ revision?: string;
30
+ } | RavelProgram): RavelSemanticIndex;
@@ -0,0 +1,252 @@
1
+ const comparePosition = (left, right) =>
2
+ (left?.line ?? 0) - (right?.line ?? 0) ||
3
+ (left?.column ?? 0) - (right?.column ?? 0) ||
4
+ (left?.offset ?? 0) - (right?.offset ?? 0);
5
+
6
+ const containsPosition = (range, position) =>
7
+ range && comparePosition(range.start, position) <= 0 &&
8
+ comparePosition(position, range.end) < 0;
9
+
10
+ const rangeSize = (range) => Number.isInteger(range?.start?.offset) &&
11
+ Number.isInteger(range?.end?.offset)
12
+ ? range.end.offset - range.start.offset
13
+ : ((range?.end?.line ?? 0) - (range?.start?.line ?? 0)) * 1_000_000 +
14
+ (range?.end?.column ?? 0) - (range?.start?.column ?? 0);
15
+
16
+ const safelyDecodedUri = (value) => {
17
+ const text = String(value);
18
+ try {
19
+ return decodeURIComponent(text);
20
+ } catch {
21
+ return text;
22
+ }
23
+ };
24
+
25
+ const sameUri = (left, right) => left === right ||
26
+ safelyDecodedUri(left) === safelyDecodedUri(right);
27
+
28
+ const contains = (source, uri, position) =>
29
+ source?.range && sameUri(source.uri, uri) && containsPosition(source.range, position);
30
+
31
+ const identityLabel = (chunk) => chunk?.identity?.chunk ?? chunk?.name ?? chunk?.id;
32
+
33
+ const chunkSymbol = (chunk) => ({
34
+ id: chunk.id,
35
+ name: chunk.name ?? identityLabel(chunk),
36
+ detail: chunk.id,
37
+ kind: "piece",
38
+ uri: chunk.source?.uri,
39
+ range: chunk.source?.range,
40
+ selectionRange: chunk.source?.range,
41
+ language: chunk.metadata?.language,
42
+ generated: chunk.generated === true,
43
+ dependencies: [...(chunk.dependencies ?? [])]
44
+ });
45
+
46
+ const directiveSymbol = (directive, index) => ({
47
+ id: "directive:" + index + ":" + directive.kind,
48
+ name: directive.name ?? directive.kind,
49
+ detail: directive.kind,
50
+ kind: "directive",
51
+ uri: directive.source?.uri,
52
+ range: directive.source?.range,
53
+ selectionRange: directive.source?.range
54
+ });
55
+
56
+ const diagnosticSymbol = (diagnostic, index) => ({
57
+ id: "diagnostic:" + index + ":" + diagnostic.code,
58
+ name: diagnostic.message,
59
+ detail: diagnostic.code,
60
+ kind: "diagnostic",
61
+ uri: diagnostic.source?.uri,
62
+ range: diagnostic.source?.range,
63
+ selectionRange: diagnostic.source?.range
64
+ });
65
+
66
+ const sortLocations = (left, right) =>
67
+ String(left.uri).localeCompare(String(right.uri)) ||
68
+ comparePosition(left.range?.start, right.range?.start) ||
69
+ comparePosition(left.range?.end, right.range?.end) ||
70
+ String(left.id).localeCompare(String(right.id));
71
+
72
+ const sourceLocation = (source) => source?.uri && source?.range
73
+ ? { uri: source.uri, range: source.range }
74
+ : null;
75
+
76
+ /**
77
+ * Build a deterministic, editor-neutral index for Ravel's own semantic
78
+ * constructs. It intentionally consumes the complete program rather than a
79
+ * bounded Explorer snapshot.
80
+ */
81
+ export const createRavelSemanticIndex = (context) => {
82
+ const program = context?.program ?? context;
83
+ const pretransform = context?.pretransform;
84
+ if (!program?.chunks || typeof program.chunks !== "object") {
85
+ throw new TypeError("createRavelSemanticIndex requires a RavelProgram or Explorer-like context.");
86
+ }
87
+ const chunks = Object.values(program.chunks).slice().sort((left, right) =>
88
+ left.id.localeCompare(right.id)
89
+ );
90
+ const chunkById = new Map(chunks.map((chunk) => [chunk.id, chunk]));
91
+ const references = [];
92
+ for (const owner of chunks) {
93
+ for (let index = 0; index < (owner.references ?? []).length; index += 1) {
94
+ const reference = owner.references[index];
95
+ references.push(Object.freeze({
96
+ id: "reference:" + owner.id + ":" + index,
97
+ kind: "reference",
98
+ ownerId: owner.id,
99
+ targetId: reference.chunk,
100
+ requested: reference.requested,
101
+ source: reference.source
102
+ }));
103
+ }
104
+ }
105
+ references.sort((left, right) => sortLocations(
106
+ { ...left.source, id: left.id },
107
+ { ...right.source, id: right.id }
108
+ ));
109
+ const directives = [...(pretransform?.directives ?? [])];
110
+ const symbols = [
111
+ ...chunks.map(chunkSymbol),
112
+ ...directives.map(directiveSymbol)
113
+ ].filter(({ uri, range }) => uri && range).sort(sortLocations);
114
+ const diagnostics = [...(program.diagnostics ?? [])];
115
+
116
+ const entityAt = (uri, position) => {
117
+ const candidates = [
118
+ ...references.filter(({ source }) => contains(source, uri, position)).map((entry) => ({
119
+ entity: entry,
120
+ source: entry.source,
121
+ priority: 0
122
+ })),
123
+ ...chunks.filter(({ source }) => contains(source, uri, position)).map((entry) => ({
124
+ entity: { id: entry.id, kind: "piece", source: entry.source },
125
+ source: entry.source,
126
+ priority: 1
127
+ })),
128
+ ...directives.map((entry, index) => ({
129
+ entity: { id: "directive:" + index + ":" + entry.kind, kind: "directive", directive: entry, source: entry.source },
130
+ source: entry.source,
131
+ priority: 2
132
+ })).filter(({ source }) => contains(source, uri, position)),
133
+ ...diagnostics.map((entry, index) => ({
134
+ entity: { ...diagnosticSymbol(entry, index), source: entry.source, diagnostic: entry },
135
+ source: entry.source,
136
+ priority: 3
137
+ })).filter(({ source }) => contains(source, uri, position))
138
+ ];
139
+ candidates.sort((left, right) =>
140
+ rangeSize(left.source.range) - rangeSize(right.source.range) ||
141
+ left.priority - right.priority ||
142
+ left.entity.id.localeCompare(right.entity.id)
143
+ );
144
+ return candidates[0]?.entity ?? null;
145
+ };
146
+
147
+ const definitionAt = (uri, position) => {
148
+ const entity = entityAt(uri, position);
149
+ if (entity?.kind !== "reference") return null;
150
+ const target = chunkById.get(entity.targetId);
151
+ const location = sourceLocation(target?.source);
152
+ return location ? { ...location, id: target.id, kind: "piece" } : null;
153
+ };
154
+
155
+ const referencesFor = (id, { includeDeclaration = false } = {}) => {
156
+ const locations = references
157
+ .filter(({ targetId }) => targetId === id)
158
+ .map((reference) => ({
159
+ ...sourceLocation(reference.source),
160
+ id: reference.id,
161
+ kind: "reference",
162
+ ownerId: reference.ownerId,
163
+ targetId: reference.targetId
164
+ }))
165
+ .filter(({ uri, range }) => uri && range);
166
+ const definition = chunkById.get(id);
167
+ const declaration = sourceLocation(definition?.source);
168
+ if (includeDeclaration && declaration) {
169
+ locations.unshift({ ...declaration, id, kind: "piece", declaration: true });
170
+ }
171
+ return locations.sort(sortLocations);
172
+ };
173
+
174
+ const hoverAt = (uri, position) => {
175
+ const entity = entityAt(uri, position);
176
+ if (!entity) return null;
177
+ const id = entity.kind === "reference" ? entity.targetId : entity.id;
178
+ const chunk = chunkById.get(id);
179
+ if (chunk) {
180
+ return {
181
+ kind: entity.kind,
182
+ range: entity.source?.range ?? chunk.source?.range,
183
+ contents: {
184
+ title: chunk.name ?? identityLabel(chunk),
185
+ canonicalId: chunk.id,
186
+ language: chunk.metadata?.language,
187
+ dependencies: [...(chunk.dependencies ?? [])],
188
+ dependents: chunks.filter(({ dependencies }) => dependencies?.includes(chunk.id)).map(({ id: owner }) => owner),
189
+ generated: chunk.generated === true,
190
+ referenceCount: references.filter(({ targetId }) => targetId === chunk.id).length
191
+ }
192
+ };
193
+ }
194
+ if (entity.kind === "directive") {
195
+ return {
196
+ kind: "directive",
197
+ range: entity.source?.range,
198
+ contents: {
199
+ title: entity.directive.kind,
200
+ directive: structuredClone(entity.directive)
201
+ }
202
+ };
203
+ }
204
+ return entity.diagnostic
205
+ ? { kind: "diagnostic", range: entity.source?.range, contents: entity.diagnostic }
206
+ : null;
207
+ };
208
+
209
+ const completeReferences = (query = "", options = {}) => {
210
+ const needle = query.toLocaleLowerCase();
211
+ return chunks
212
+ .filter((chunk) => options.documentId === undefined ||
213
+ chunk.identity?.document === options.documentId)
214
+ .filter((chunk) => !needle ||
215
+ chunk.id.toLocaleLowerCase().includes(needle) ||
216
+ String(chunk.name ?? "").toLocaleLowerCase().includes(needle))
217
+ .map((chunk) => ({
218
+ label: chunk.id,
219
+ detail: chunk.name ?? identityLabel(chunk),
220
+ kind: "piece",
221
+ language: chunk.metadata?.language,
222
+ insertText: chunk.id
223
+ }));
224
+ };
225
+
226
+ const documentSymbols = (uri) => symbols.filter((symbol) => sameUri(symbol.uri, uri));
227
+
228
+ return Object.freeze({
229
+ revision: context?.revision,
230
+ chunks: Object.freeze(chunks),
231
+ references: Object.freeze(references),
232
+ directives: Object.freeze(directives),
233
+ diagnostics: Object.freeze(diagnostics),
234
+ symbols: Object.freeze(symbols),
235
+ entityAt,
236
+ definitionAt,
237
+ referencesFor,
238
+ hoverAt,
239
+ completeReferences,
240
+ documentSymbols,
241
+ workspaceSymbols(query = "") {
242
+ const needle = query.toLocaleLowerCase();
243
+ return symbols.filter(({ name, detail }) =>
244
+ !needle || name.toLocaleLowerCase().includes(needle) ||
245
+ String(detail).toLocaleLowerCase().includes(needle)
246
+ );
247
+ },
248
+ diagnosticsFor(uri) {
249
+ return diagnostics.filter(({ source }) => sameUri(source?.uri, uri));
250
+ }
251
+ });
252
+ };