@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.
- package/LICENSE +21 -0
- package/README.md +180 -0
- package/package.json +48 -0
- package/src/edits.d.ts +29 -0
- package/src/edits.js +397 -0
- package/src/index.d.ts +48 -0
- package/src/index.js +881 -0
- package/src/ravel-index.d.ts +30 -0
- package/src/ravel-index.js +252 -0
|
@@ -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
|
+
};
|