@heroiclands/content-language-server 0.1.0 → 0.1.1
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/README.md +2 -0
- package/docs/content-language-server.md +18 -2
- package/engine/content-language-server.mjs +285 -55
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,4 +8,6 @@ Install an exact released package version in a directory managed by your editor
|
|
|
8
8
|
|
|
9
9
|
See the [language server guide](docs/content-language-server.md) for LSP requests, cache locations, error handling, and manual recovery commands.
|
|
10
10
|
|
|
11
|
+
Editors can pass explicit foreign content roots in LSP initialization options. A normal workspace-symbol query stays in the current project; an `all:` query searches the configured roots as well. The server returns standard LSP symbols and locations, so the editor controls how results are presented.
|
|
12
|
+
|
|
11
13
|
Maintainers use the [publishing guide](docs/publishing.md) for the first npm release and trusted publisher configuration.
|
|
@@ -23,8 +23,24 @@ The first prints the private JSONL path. The second is manual recovery when a fi
|
|
|
23
23
|
| `workspace/symbol` | Finds notes by name, alias, ASCII name, shortcode, or Address. `tag:myth` searches tags. One result appears per source note. |
|
|
24
24
|
| `textDocument/references` | Finds authored wikilinks, embeds, and declared frontmatter Address values or keys. Ordinary prose is excluded. |
|
|
25
25
|
|
|
26
|
-
Reference search
|
|
26
|
+
Reference search uses indexed frontmatter to select candidates and reads saved Markdown source for exact ranges and body links. Unsaved edits identify the target under the cursor but do not enter workspace search. The server writes only LSP messages to stdout and uses UTF-16 positions.
|
|
27
|
+
|
|
28
|
+
## Foreign content projects
|
|
29
|
+
|
|
30
|
+
An LSP client selects foreign project roots through `initialize`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"initializationOptions": {
|
|
35
|
+
"foreignRoots": ["/absolute/path/to/another/content/project"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The paths name repositories containing their own `package-build.config.yaml`, `.yml`, or `.mjs`. They are editor search configuration, independent of a package's build dependencies. The server validates each foreign project's private index when first used and rebuilds it from saved source if missing or incompatible. A valid complete cache is reusable. An unconfigured cache never adds a project to search. A failed foreign root produces an LSP status message while available projects remain searchable. Save and file-operation notifications refresh the affected project. Clients can update the root list with `workspace/didChangeConfiguration` using `settings.heroiclands.foreignRoots`.
|
|
41
|
+
|
|
42
|
+
Plain `workspace/symbol` queries search the current project. Prefix the query with `all:` to include configured foreign projects; `all:tag:myth` searches tags in that scope. Results name the owning package and open its source note. Package-qualified definitions open a note or asset from its owning root. A bare Address with matches in multiple configured projects returns all destinations for the client to present. `textDocument/references` searches the current and configured foreign projects and reports exact saved source ranges.
|
|
27
43
|
|
|
28
44
|
## Editor integration
|
|
29
45
|
|
|
30
|
-
An LSP client starts the executable with the content project as its working directory and associates it with Markdown notes under the configured content directory. The process handles `initialize`, `shutdown`, `exit`, full and incremental document synchronization, save and file-operation notifications, definition, references, and workspace symbols. It does not advertise completion, diagnostics, rename, or document symbols. An editor integration supplies its own installation, project discovery, and UI commands.
|
|
46
|
+
An LSP client starts the executable with the content project as its working directory and associates it with Markdown notes under the configured content directory. The process handles `initialize`, `shutdown`, `exit`, full and incremental document synchronization, save and file-operation notifications, configuration changes, definition, references, and workspace symbols. It does not advertise completion, diagnostics, rename, or document symbols. An editor integration supplies its own installation, project discovery, and UI commands.
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
7
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
9
|
import YAML from "yaml";
|
|
9
10
|
import { parseAddress, renderAddress } from "@heroiclands/package-build/engine/address";
|
|
10
11
|
import { addressPositions } from "@heroiclands/package-build/engine/note-addresses";
|
|
11
12
|
import { parseWikilink, WIKILINK } from "@heroiclands/package-build/engine/wikilink-syntax";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
CONFIG_FILENAMES,
|
|
15
|
+
configFromData,
|
|
16
|
+
loadPackConfig,
|
|
17
|
+
} from "@heroiclands/package-build/engine/pack-config";
|
|
13
18
|
import { noteFile } from "@heroiclands/package-build/engine/index-records";
|
|
14
19
|
import { NOTE_VOCABULARY } from "@heroiclands/package-build/engine/note-vocabulary";
|
|
15
20
|
import {
|
|
@@ -21,6 +26,22 @@ import { matchAllOutsideCode } from "@heroiclands/package-build/engine/code-fenc
|
|
|
21
26
|
import { embedsIn, EMBED_DEFAULT_TYPE } from "@heroiclands/package-build/engine/content-embeds";
|
|
22
27
|
|
|
23
28
|
const EMPTY_RANGE = { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } };
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
|
|
31
|
+
/** Load an explicitly selected project's configuration with this server's toolchain. */
|
|
32
|
+
function loadForeignConfig(root) {
|
|
33
|
+
const files = CONFIG_FILENAMES.map((name) => path.join(root, name)).filter((file) =>
|
|
34
|
+
fs.existsSync(file),
|
|
35
|
+
);
|
|
36
|
+
if (files.length !== 1)
|
|
37
|
+
throw new Error(root + " must contain exactly one package-build configuration");
|
|
38
|
+
const file = files[0];
|
|
39
|
+
if (file.endsWith(".mjs")) {
|
|
40
|
+
const module = require(file);
|
|
41
|
+
return module.default ?? module;
|
|
42
|
+
}
|
|
43
|
+
return configFromData(YAML.parse(fs.readFileSync(file, "utf8")), file);
|
|
44
|
+
}
|
|
24
45
|
|
|
25
46
|
/** Return an LSP position for a UTF-16 offset in TEXT. */
|
|
26
47
|
function positionAt(text, offset) {
|
|
@@ -52,8 +73,13 @@ function noteName(record) {
|
|
|
52
73
|
|
|
53
74
|
/** An index read from the package's configured content tree. */
|
|
54
75
|
export class ContentWorkspace {
|
|
55
|
-
constructor(
|
|
76
|
+
constructor(
|
|
77
|
+
config = loadPackConfig(),
|
|
78
|
+
{ cacheBase, onStatus = () => {}, loadProjectConfig = loadForeignConfig } = {},
|
|
79
|
+
) {
|
|
56
80
|
this.config = config;
|
|
81
|
+
this.cacheBase = cacheBase;
|
|
82
|
+
this.loadProjectConfig = loadProjectConfig;
|
|
57
83
|
this.contentRoot = config.paths.content;
|
|
58
84
|
this.cacheDirectory = languageIndexDirectory(config, cacheBase);
|
|
59
85
|
this.indexFile = path.join(this.cacheDirectory, "metadata.jsonl");
|
|
@@ -66,6 +92,123 @@ export class ContentWorkspace {
|
|
|
66
92
|
this.byFile = new Map();
|
|
67
93
|
this.types = new Set(Object.keys(NOTE_VOCABULARY));
|
|
68
94
|
this.documents = new Map();
|
|
95
|
+
this.foreignRoots = [];
|
|
96
|
+
this.foreign = new Map();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Select foreign roots explicitly; a cache directory cannot add a project. */
|
|
100
|
+
configureForeignRoots(roots = []) {
|
|
101
|
+
if (!Array.isArray(roots) || roots.some((root) => typeof root !== "string"))
|
|
102
|
+
throw new Error("initializationOptions.foreignRoots must be an array of paths");
|
|
103
|
+
const ownRoot = fs.realpathSync(this.config.rootDir);
|
|
104
|
+
this.foreignRoots = [
|
|
105
|
+
...new Set(
|
|
106
|
+
roots.map((root) => {
|
|
107
|
+
const absolute = path.resolve(root);
|
|
108
|
+
return fs.existsSync(absolute) ? fs.realpathSync(absolute) : absolute;
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
].filter((root) => root !== ownRoot);
|
|
112
|
+
for (const [root, project] of this.foreign)
|
|
113
|
+
if (!this.foreignRoots.includes(root)) {
|
|
114
|
+
project.close();
|
|
115
|
+
this.foreign.delete(root);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
foreignWorkspace(root) {
|
|
120
|
+
if (this.foreign.has(root)) return this.foreign.get(root);
|
|
121
|
+
const config = this.loadProjectConfig(root);
|
|
122
|
+
const project = new ContentWorkspace(config, {
|
|
123
|
+
cacheBase: this.cacheBase,
|
|
124
|
+
onStatus: (message) => {
|
|
125
|
+
if (message) this.onStatus(config.contentPackage + ": " + message);
|
|
126
|
+
},
|
|
127
|
+
loadProjectConfig: this.loadProjectConfig,
|
|
128
|
+
});
|
|
129
|
+
project.documents = this.documents;
|
|
130
|
+
this.foreign.set(root, project);
|
|
131
|
+
return project;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Return usable indexes, reporting one failed root without hiding the others. */
|
|
135
|
+
indexedWorkspaces(includeForeign = false) {
|
|
136
|
+
this.requireIndex();
|
|
137
|
+
const projects = [this];
|
|
138
|
+
if (includeForeign)
|
|
139
|
+
for (const root of this.foreignRoots) {
|
|
140
|
+
try {
|
|
141
|
+
const project = this.foreignWorkspace(root);
|
|
142
|
+
project.start(true);
|
|
143
|
+
project.requireIndex();
|
|
144
|
+
projects.push(project);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
this.onStatus("Foreign content project " + root + ": " + error.message);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return projects;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Find every indexed owner of a written Address for navigation. */
|
|
153
|
+
resolveCandidates(value, defaults = {}, projects = [this]) {
|
|
154
|
+
const packages = new Set(projects.map((project) => project.config.contentPackage));
|
|
155
|
+
const found = [];
|
|
156
|
+
for (const project of projects) {
|
|
157
|
+
const tuple = parseAddress(value, {
|
|
158
|
+
package: project.config.contentPackage,
|
|
159
|
+
system: "none",
|
|
160
|
+
types: project.types,
|
|
161
|
+
packages,
|
|
162
|
+
...defaults,
|
|
163
|
+
});
|
|
164
|
+
if (tuple.reason) continue;
|
|
165
|
+
const record = project.byAddress.get(renderAddress(tuple));
|
|
166
|
+
if (record) found.push({ record, project });
|
|
167
|
+
}
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Locate the workspace that owns an open source document. */
|
|
172
|
+
sourceWorkspace(uri, projects) {
|
|
173
|
+
const file = fileURLToPath(uri);
|
|
174
|
+
return (
|
|
175
|
+
projects.find((project) => {
|
|
176
|
+
const relative = path.relative(project.contentRoot, file);
|
|
177
|
+
return (
|
|
178
|
+
relative &&
|
|
179
|
+
!path.isAbsolute(relative) &&
|
|
180
|
+
relative !== ".." &&
|
|
181
|
+
!relative.startsWith(".." + path.sep)
|
|
182
|
+
);
|
|
183
|
+
}) ?? this
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
scheduleRebuildForUri(uri) {
|
|
188
|
+
const file = fileURLToPath(uri);
|
|
189
|
+
const inside = (directory) => {
|
|
190
|
+
const relative = path.relative(directory, file);
|
|
191
|
+
return (
|
|
192
|
+
relative &&
|
|
193
|
+
!path.isAbsolute(relative) &&
|
|
194
|
+
relative !== ".." &&
|
|
195
|
+
!relative.startsWith(".." + path.sep)
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
if (inside(this.contentRoot) || inside(this.config.paths.assets)) {
|
|
199
|
+
this.scheduleRebuild();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const root of this.foreignRoots) {
|
|
203
|
+
if (!inside(root)) continue;
|
|
204
|
+
try {
|
|
205
|
+
const project = this.foreignWorkspace(root);
|
|
206
|
+
if (inside(project.contentRoot) || inside(project.config.paths.assets))
|
|
207
|
+
project.scheduleRebuild();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.onStatus("Foreign content project " + root + ": " + error.message);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
69
212
|
}
|
|
70
213
|
|
|
71
214
|
refresh() {
|
|
@@ -126,9 +269,15 @@ export class ContentWorkspace {
|
|
|
126
269
|
}
|
|
127
270
|
}
|
|
128
271
|
|
|
129
|
-
start() {
|
|
272
|
+
start(acceptCompleteCache = false) {
|
|
130
273
|
if (this.started) return;
|
|
131
274
|
this.started = true;
|
|
275
|
+
if (acceptCompleteCache)
|
|
276
|
+
try {
|
|
277
|
+
if (this.refresh()) return;
|
|
278
|
+
} catch {
|
|
279
|
+
// A corrupt or incompatible snapshot is rebuilt from saved notes.
|
|
280
|
+
}
|
|
132
281
|
this.rebuild();
|
|
133
282
|
}
|
|
134
283
|
|
|
@@ -143,6 +292,7 @@ export class ContentWorkspace {
|
|
|
143
292
|
close() {
|
|
144
293
|
if (this.rebuildTimer) clearTimeout(this.rebuildTimer);
|
|
145
294
|
this.rebuildTimer = null;
|
|
295
|
+
for (const project of this.foreign.values()) project.close();
|
|
146
296
|
}
|
|
147
297
|
|
|
148
298
|
requireIndex() {
|
|
@@ -175,31 +325,39 @@ export class ContentWorkspace {
|
|
|
175
325
|
}
|
|
176
326
|
|
|
177
327
|
/** Resolve a written Address using the same tuple grammar as the build. */
|
|
178
|
-
resolve(value, defaults = {}) {
|
|
328
|
+
resolve(value, defaults = {}, projects = [this]) {
|
|
179
329
|
const tuple = parseAddress(value, {
|
|
180
330
|
package: this.config.contentPackage,
|
|
181
331
|
system: "none",
|
|
182
|
-
types:
|
|
183
|
-
packages: new Set(
|
|
332
|
+
types: new Set(projects.flatMap((project) => [...project.types])),
|
|
333
|
+
packages: new Set(projects.map((project) => project.config.contentPackage)),
|
|
184
334
|
...defaults,
|
|
185
335
|
});
|
|
186
336
|
if (tuple.reason) return null;
|
|
187
|
-
|
|
337
|
+
for (const project of projects)
|
|
338
|
+
if (project.config.contentPackage === tuple.package) {
|
|
339
|
+
const record = project.byAddress.get(renderAddress(tuple));
|
|
340
|
+
if (record) return record;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
188
343
|
}
|
|
189
344
|
|
|
190
345
|
/** Link and declared frontmatter targets, with exact source ranges. */
|
|
191
|
-
referencesInText(text, file) {
|
|
346
|
+
referencesInText(text, file, projects = [this], includeFrontmatter = true) {
|
|
192
347
|
const found = [];
|
|
193
348
|
const bodyStart = text.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/)?.[0].length ?? 0;
|
|
194
349
|
const linkText = text.slice(bodyStart);
|
|
195
350
|
for (const match of matchAllOutsideCode(linkText, new RegExp(WIKILINK.source, "g"))) {
|
|
196
351
|
const parsed = parseWikilink(match[1]);
|
|
197
|
-
const record =
|
|
352
|
+
const record =
|
|
353
|
+
parsed.target ? this.resolve(parsed.target, {}, projects) : this.byFile.get(file);
|
|
198
354
|
if (!record) continue;
|
|
199
355
|
const start = bodyStart + match.index + (parsed.target ? 2 : 3);
|
|
200
356
|
found.push({
|
|
201
357
|
record,
|
|
202
358
|
anchor: parsed.anchor,
|
|
359
|
+
written: parsed.target,
|
|
360
|
+
defaults: {},
|
|
203
361
|
location: location(
|
|
204
362
|
file,
|
|
205
363
|
text,
|
|
@@ -209,16 +367,19 @@ export class ContentWorkspace {
|
|
|
209
367
|
});
|
|
210
368
|
}
|
|
211
369
|
for (const embed of embedsIn(linkText)) {
|
|
212
|
-
const
|
|
370
|
+
const defaults = { type: EMBED_DEFAULT_TYPE };
|
|
371
|
+
const record = this.resolve(embed.written, defaults, projects);
|
|
213
372
|
if (!record) continue;
|
|
214
373
|
const start = bodyStart + embed.index + 3;
|
|
215
374
|
found.push({
|
|
216
375
|
record,
|
|
217
376
|
anchor: "",
|
|
377
|
+
written: embed.written,
|
|
378
|
+
defaults,
|
|
218
379
|
location: location(file, text, start, start + embed.written.length),
|
|
219
380
|
});
|
|
220
381
|
}
|
|
221
|
-
if (!bodyStart) return found;
|
|
382
|
+
if (!bodyStart || !includeFrontmatter) return found;
|
|
222
383
|
const yamlStart = text.indexOf("\n") + 1;
|
|
223
384
|
const yamlText = text.slice(yamlStart, bodyStart).replace(/\r?\n---(?:\r?\n)?$/, "");
|
|
224
385
|
let document;
|
|
@@ -249,15 +410,18 @@ export class ContentWorkspace {
|
|
|
249
410
|
}
|
|
250
411
|
const scalar = (part, value) => {
|
|
251
412
|
if (!YAML.isScalar(part) || typeof value !== "string" || !part.range) return;
|
|
252
|
-
const
|
|
413
|
+
const defaults = {
|
|
253
414
|
system: position.system ?? "none",
|
|
254
415
|
type: position.type,
|
|
255
|
-
}
|
|
416
|
+
};
|
|
417
|
+
const record = this.resolve(value, defaults, projects);
|
|
256
418
|
if (!record) return;
|
|
257
419
|
const start = yamlStart + part.range[0];
|
|
258
420
|
found.push({
|
|
259
421
|
record,
|
|
260
422
|
anchor: "",
|
|
423
|
+
written: value,
|
|
424
|
+
defaults,
|
|
261
425
|
location: location(file, text, start, yamlStart + part.range[1]),
|
|
262
426
|
});
|
|
263
427
|
};
|
|
@@ -276,49 +440,75 @@ export class ContentWorkspace {
|
|
|
276
440
|
return found;
|
|
277
441
|
}
|
|
278
442
|
|
|
279
|
-
targetAt(uri, position) {
|
|
443
|
+
targetAt(uri, position, projects = [this]) {
|
|
280
444
|
const text = this.text(uri);
|
|
281
445
|
if (text == null) return null;
|
|
282
446
|
const offset = offsetAt(text, position);
|
|
283
447
|
if (offset < 0) return null;
|
|
284
448
|
const file = fileURLToPath(uri);
|
|
285
|
-
const reference = this.referencesInText(text, file).find(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
449
|
+
const reference = this.referencesInText(text, file, projects).find(
|
|
450
|
+
({ location: source }) => {
|
|
451
|
+
const start = offsetAt(text, source.range.start);
|
|
452
|
+
const end = offsetAt(text, source.range.end);
|
|
453
|
+
return start <= offset && offset <= end;
|
|
454
|
+
},
|
|
455
|
+
);
|
|
456
|
+
if (reference) return reference;
|
|
291
457
|
const own = this.byFile.get(file);
|
|
292
458
|
if (own && /^shortcode:\s*/.test(text.split("\n")[position.line] ?? ""))
|
|
293
|
-
return { record: own, anchor: "" };
|
|
294
|
-
const start = text.slice(0, offset).search(/[A-Za-z0-9
|
|
459
|
+
return { record: own, anchor: "", written: own.address?.canonical, defaults: {} };
|
|
460
|
+
const start = text.slice(0, offset).search(/[A-Za-z0-9./-]+$/);
|
|
295
461
|
if (start < 0) return null;
|
|
296
|
-
const end = offset + (text.slice(offset).match(/^[A-Za-z0-9
|
|
297
|
-
const
|
|
298
|
-
|
|
462
|
+
const end = offset + (text.slice(offset).match(/^[A-Za-z0-9./-]+/)?.[0].length ?? 0);
|
|
463
|
+
const written = text.slice(start, end);
|
|
464
|
+
const record = this.resolve(written, {}, projects);
|
|
465
|
+
return { record, anchor: "", written, defaults: {} };
|
|
299
466
|
}
|
|
300
467
|
|
|
301
468
|
definition(uri, position) {
|
|
302
|
-
this.
|
|
303
|
-
const
|
|
469
|
+
const projects = this.indexedWorkspaces(true);
|
|
470
|
+
const source = this.sourceWorkspace(uri, projects);
|
|
471
|
+
const target = source.targetAt(uri, position, projects);
|
|
304
472
|
if (!target) return null;
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
473
|
+
const candidates =
|
|
474
|
+
target.written ?
|
|
475
|
+
source.resolveCandidates(target.written, target.defaults, projects)
|
|
476
|
+
: [{ record: target.record, project: source }];
|
|
477
|
+
const locations = [];
|
|
478
|
+
for (const { record, project } of candidates) {
|
|
479
|
+
const file = project.fileFor(record);
|
|
480
|
+
if (!file) continue;
|
|
481
|
+
if (record.asset) {
|
|
482
|
+
locations.push({ uri: pathToFileURL(file).href, range: EMPTY_RANGE });
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
const line =
|
|
486
|
+
target.anchor ?
|
|
487
|
+
record.anchors?.find(
|
|
488
|
+
(entry) => entry.slug.toLowerCase() === target.anchor.toLowerCase(),
|
|
489
|
+
)?.line
|
|
490
|
+
: null;
|
|
491
|
+
if (target.anchor && !line) continue;
|
|
492
|
+
const targetText = fs.readFileSync(file, "utf8");
|
|
493
|
+
const offset = line ? offsetAt(targetText, { line: line - 1, character: 0 }) : 0;
|
|
494
|
+
locations.push(location(file, targetText, offset, offset));
|
|
495
|
+
}
|
|
496
|
+
return (
|
|
497
|
+
locations.length === 1 ? locations[0]
|
|
498
|
+
: locations.length ? locations
|
|
499
|
+
: null
|
|
500
|
+
);
|
|
318
501
|
}
|
|
319
502
|
|
|
320
503
|
symbols(query) {
|
|
321
|
-
|
|
504
|
+
const includeForeign = /^all:/i.test(query);
|
|
505
|
+
const search = includeForeign ? query.slice(4) : query;
|
|
506
|
+
return this.indexedWorkspaces(includeForeign).flatMap((project) =>
|
|
507
|
+
project.symbolMatches(search),
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
symbolMatches(query) {
|
|
322
512
|
const tag = /^tag:(.*)$/i.exec(query);
|
|
323
513
|
const needle = (tag ? tag[1] : query).trim().toLowerCase();
|
|
324
514
|
const found = new Map();
|
|
@@ -345,7 +535,7 @@ export class ContentWorkspace {
|
|
|
345
535
|
found.set(file, {
|
|
346
536
|
name: String(noteName(record)),
|
|
347
537
|
kind: 1,
|
|
348
|
-
containerName: `${record.address?.slug ?? [record.type, record.shortcode].filter(Boolean).join(" ")} · ${tag ? `tag: ${match}` : match}`,
|
|
538
|
+
containerName: `${record.package} · ${record.address?.slug ?? [record.type, record.shortcode].filter(Boolean).join(" ")} · ${tag ? `tag: ${match}` : match}`,
|
|
349
539
|
location: { uri: pathToFileURL(file).href, range: EMPTY_RANGE },
|
|
350
540
|
});
|
|
351
541
|
}
|
|
@@ -353,29 +543,57 @@ export class ContentWorkspace {
|
|
|
353
543
|
}
|
|
354
544
|
|
|
355
545
|
references(uri, position) {
|
|
356
|
-
this.
|
|
357
|
-
const
|
|
358
|
-
|
|
546
|
+
const projects = this.indexedWorkspaces(true);
|
|
547
|
+
const source = this.sourceWorkspace(uri, projects);
|
|
548
|
+
const target = source.targetAt(uri, position, projects);
|
|
549
|
+
if (!target) return [];
|
|
550
|
+
const candidates =
|
|
551
|
+
target.record ?
|
|
552
|
+
[target.record]
|
|
553
|
+
: source
|
|
554
|
+
.resolveCandidates(target.written, target.defaults, projects)
|
|
555
|
+
.map(({ record }) => record);
|
|
556
|
+
const addresses = new Set(
|
|
557
|
+
candidates.map((record) => record.address?.canonical).filter(Boolean),
|
|
558
|
+
);
|
|
559
|
+
if (!addresses.size) return [];
|
|
359
560
|
const locations = [];
|
|
360
|
-
const
|
|
361
|
-
const visit = (directory) => {
|
|
561
|
+
const needles = candidates.map((record) => record.shortcode.toLowerCase());
|
|
562
|
+
const visit = (project, directory, frontmatterCandidates) => {
|
|
362
563
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
363
564
|
if (entry.name.startsWith(".")) continue;
|
|
364
565
|
const file = path.join(directory, entry.name);
|
|
365
566
|
if (entry.isDirectory()) {
|
|
366
|
-
visit(file);
|
|
567
|
+
visit(project, file, frontmatterCandidates);
|
|
367
568
|
} else if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
368
569
|
const savedText = fs.readFileSync(file, "utf8");
|
|
369
|
-
if (!savedText.toLowerCase().includes(needle))
|
|
370
|
-
|
|
371
|
-
for (const reference of
|
|
372
|
-
|
|
570
|
+
if (!needles.some((needle) => savedText.toLowerCase().includes(needle)))
|
|
571
|
+
continue;
|
|
572
|
+
for (const reference of project.referencesInText(
|
|
573
|
+
savedText,
|
|
574
|
+
file,
|
|
575
|
+
projects,
|
|
576
|
+
frontmatterCandidates.has(file),
|
|
577
|
+
)) {
|
|
578
|
+
if (addresses.has(reference.record.address?.canonical))
|
|
373
579
|
locations.push(reference.location);
|
|
374
580
|
}
|
|
375
581
|
}
|
|
376
582
|
}
|
|
377
583
|
};
|
|
378
|
-
|
|
584
|
+
for (const project of projects) {
|
|
585
|
+
const frontmatterCandidates = new Set(
|
|
586
|
+
project.records
|
|
587
|
+
.filter((record) =>
|
|
588
|
+
needles.some((needle) =>
|
|
589
|
+
JSON.stringify(record).toLowerCase().includes(needle),
|
|
590
|
+
),
|
|
591
|
+
)
|
|
592
|
+
.filter((record) => record.file?.path)
|
|
593
|
+
.map((record) => noteFile(project.contentRoot, record)),
|
|
594
|
+
);
|
|
595
|
+
visit(project, project.contentRoot, frontmatterCandidates);
|
|
596
|
+
}
|
|
379
597
|
return locations;
|
|
380
598
|
}
|
|
381
599
|
}
|
|
@@ -385,6 +603,7 @@ export function respond(workspace, message) {
|
|
|
385
603
|
const { method, params = {} } = message;
|
|
386
604
|
switch (method) {
|
|
387
605
|
case "initialize":
|
|
606
|
+
workspace.configureForeignRoots(params.initializationOptions?.foreignRoots ?? []);
|
|
388
607
|
workspace.start();
|
|
389
608
|
return {
|
|
390
609
|
capabilities: {
|
|
@@ -405,6 +624,10 @@ export function respond(workspace, message) {
|
|
|
405
624
|
};
|
|
406
625
|
case "shutdown":
|
|
407
626
|
return null;
|
|
627
|
+
case "workspace/didChangeConfiguration":
|
|
628
|
+
if (params.settings?.heroiclands?.foreignRoots)
|
|
629
|
+
workspace.configureForeignRoots(params.settings.heroiclands.foreignRoots);
|
|
630
|
+
return undefined;
|
|
408
631
|
case "textDocument/didOpen":
|
|
409
632
|
workspace.documents.set(params.textDocument.uri, params.textDocument.text);
|
|
410
633
|
return undefined;
|
|
@@ -426,12 +649,19 @@ export function respond(workspace, message) {
|
|
|
426
649
|
workspace.documents.delete(params.textDocument.uri);
|
|
427
650
|
return undefined;
|
|
428
651
|
case "textDocument/didSave":
|
|
652
|
+
workspace.scheduleRebuildForUri(params.textDocument.uri);
|
|
653
|
+
return undefined;
|
|
429
654
|
case "workspace/didChangeWatchedFiles":
|
|
430
655
|
case "workspace/didCreateFiles":
|
|
431
656
|
case "workspace/didRenameFiles":
|
|
432
|
-
case "workspace/didDeleteFiles":
|
|
433
|
-
|
|
657
|
+
case "workspace/didDeleteFiles": {
|
|
658
|
+
const files = params.changes ?? params.files ?? [];
|
|
659
|
+
if (!files.length) workspace.scheduleRebuild();
|
|
660
|
+
for (const item of files)
|
|
661
|
+
for (const uri of [item.uri, item.oldUri, item.newUri].filter(Boolean))
|
|
662
|
+
workspace.scheduleRebuildForUri(uri);
|
|
434
663
|
return undefined;
|
|
664
|
+
}
|
|
435
665
|
case "textDocument/definition":
|
|
436
666
|
return workspace.definition(params.textDocument.uri, params.position);
|
|
437
667
|
case "textDocument/references":
|