@spyglassmc/language-server 0.4.55 → 0.4.57
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/lib/server.js +93 -14
- package/lib/util/LspFileWatcher.d.ts +33 -0
- package/lib/util/LspFileWatcher.js +226 -0
- package/lib/util/toLS.d.ts +3 -1
- package/lib/util/toLS.js +2 -2
- package/lib/util/types.d.ts +2 -1
- package/package.json +5 -5
package/lib/server.js
CHANGED
|
@@ -9,6 +9,7 @@ import url from 'url';
|
|
|
9
9
|
import * as util from 'util';
|
|
10
10
|
import * as ls from 'vscode-languageserver/node.js';
|
|
11
11
|
import { toCore, toLS } from './util/index.js';
|
|
12
|
+
import { LspFileWatcher } from './util/LspFileWatcher.js';
|
|
12
13
|
export * from './util/types.js';
|
|
13
14
|
if (process.argv.length === 2) {
|
|
14
15
|
// When the server is launched from the cmd script, the process arguments
|
|
@@ -21,6 +22,7 @@ const cacheRoot = fileUtil.ensureEndingSlash(url.pathToFileURL(cacheRootPath).to
|
|
|
21
22
|
const connection = ls.createConnection();
|
|
22
23
|
let capabilities;
|
|
23
24
|
let workspaceFolders;
|
|
25
|
+
let projectRoots;
|
|
24
26
|
let hasShutdown = false;
|
|
25
27
|
const externals = getNodeJsExternals({ cacheRoot });
|
|
26
28
|
const logger = {
|
|
@@ -30,9 +32,17 @@ const logger = {
|
|
|
30
32
|
warn: (msg, ...args) => connection.console.warn(util.format(msg, ...args)),
|
|
31
33
|
};
|
|
32
34
|
let service;
|
|
33
|
-
function buildSemanticTokensCapability() {
|
|
35
|
+
function buildSemanticTokensCapability(isDynamic) {
|
|
36
|
+
// Always register everything for static registration, so all changes to the config can be
|
|
37
|
+
// processed by the request handlers instead
|
|
38
|
+
const semanticTokensConfig = service.project.config.env.feature.semanticColoring;
|
|
39
|
+
let disabledLanguages = [];
|
|
40
|
+
if (isDynamic && typeof semanticTokensConfig === 'object'
|
|
41
|
+
&& Array.isArray(semanticTokensConfig.disabledLanguages)) {
|
|
42
|
+
disabledLanguages = semanticTokensConfig.disabledLanguages;
|
|
43
|
+
}
|
|
34
44
|
return {
|
|
35
|
-
documentSelector: toLS.documentSelector(service.project.meta),
|
|
45
|
+
documentSelector: toLS.documentSelector(service.project.meta, { disabledLanguages }),
|
|
36
46
|
legend: toLS.semanticTokensLegend(),
|
|
37
47
|
full: { delta: false },
|
|
38
48
|
range: true,
|
|
@@ -45,6 +55,7 @@ connection.onInitialize(async (params) => {
|
|
|
45
55
|
logger.info(`[onInitialize] initializationOptions = ${JSON.stringify(initializationOptions)}`);
|
|
46
56
|
capabilities = params.capabilities;
|
|
47
57
|
workspaceFolders = params.workspaceFolders ?? [];
|
|
58
|
+
projectRoots = workspaceFolders.map(f => core.fileUtil.ensureEndingSlash(f.uri));
|
|
48
59
|
if (initializationOptions?.inDevelopmentMode) {
|
|
49
60
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
50
61
|
logger.warn('Delayed 3 seconds manually. If you see this in production, it means SPGoding messed up.');
|
|
@@ -67,13 +78,11 @@ connection.onInitialize(async (params) => {
|
|
|
67
78
|
'project#ready#bind',
|
|
68
79
|
]),
|
|
69
80
|
project: {
|
|
70
|
-
defaultConfig: core.ConfigService.merge(core.VanillaConfig, {
|
|
71
|
-
env: { gameVersion: initializationOptions?.gameVersion },
|
|
72
|
-
}),
|
|
81
|
+
defaultConfig: core.ConfigService.merge(core.VanillaConfig, initializationOptions?.defaultConfig ?? {}),
|
|
73
82
|
cacheRoot,
|
|
74
83
|
externals,
|
|
75
84
|
initializers: [mcdoc.initialize, je.initialize],
|
|
76
|
-
projectRoots
|
|
85
|
+
projectRoots,
|
|
77
86
|
},
|
|
78
87
|
});
|
|
79
88
|
service.project.on('documentErrored', async ({ errors, uri, version }) => {
|
|
@@ -101,7 +110,7 @@ connection.onInitialize(async (params) => {
|
|
|
101
110
|
let semanticTokensProvider = undefined;
|
|
102
111
|
if (!capabilities.textDocument?.semanticTokens?.dynamicRegistration) {
|
|
103
112
|
logger.info("[startDynamicSemanticTokensRegistration] LanguageClient didn't permit dynamic registration for semantic tokens. Registering semantic tokens statically instead...");
|
|
104
|
-
semanticTokensProvider = buildSemanticTokensCapability();
|
|
113
|
+
semanticTokensProvider = buildSemanticTokensCapability(false);
|
|
105
114
|
}
|
|
106
115
|
const customCapabilities = {
|
|
107
116
|
dataHackPubify: true,
|
|
@@ -141,8 +150,47 @@ connection.onInitialized(async () => {
|
|
|
141
150
|
if (capabilities.textDocument?.formatting?.dynamicRegistration) {
|
|
142
151
|
void connection.client.register(ls.DocumentFormattingRequest.type, { documentSelector: [{ language: 'mcdoc' }] });
|
|
143
152
|
}
|
|
153
|
+
if (capabilities.workspace?.didChangeConfiguration?.dynamicRegistration) {
|
|
154
|
+
void connection.client.register(ls.DidChangeConfigurationNotification.type, { section: ['spyglassmc'] });
|
|
155
|
+
}
|
|
156
|
+
// In case the initializationOptions were incomplete (for example because the client doesn't support them)
|
|
157
|
+
await updateEditorConfiguration();
|
|
144
158
|
startDynamicSemanticTokensRegistration();
|
|
145
|
-
|
|
159
|
+
// Initializes LspFileWatcher only when client supports didChangeWatchedFiles notifications.
|
|
160
|
+
const fileWatcher = capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration
|
|
161
|
+
? new LspFileWatcher({
|
|
162
|
+
capabilities,
|
|
163
|
+
connection,
|
|
164
|
+
externals,
|
|
165
|
+
locations: projectRoots,
|
|
166
|
+
logger,
|
|
167
|
+
predicate: (uri) => !service.project.shouldExclude(uri),
|
|
168
|
+
})
|
|
169
|
+
.on('ready', () => logger.info('[FileWatcher] ready'))
|
|
170
|
+
.on('add', (uri) => logger.info('[FileWatcher] added', uri))
|
|
171
|
+
.on('change', (uri) => logger.info('[FileWatcher] changed', uri))
|
|
172
|
+
.on('unlink', (uri) => logger.info('[FileWatcher] unlinked', uri))
|
|
173
|
+
.on('error', (e) => logger.error('[FileWatcher]', e))
|
|
174
|
+
: undefined;
|
|
175
|
+
if (fileWatcher) {
|
|
176
|
+
// Listen for config changes and reconcile the internal state of the file watcher if
|
|
177
|
+
// `env.exclude` has changed.
|
|
178
|
+
service.project.on('configChanged', async ({ oldConfig, newConfig }) => {
|
|
179
|
+
const oldExclude = new Set(oldConfig.env.exclude);
|
|
180
|
+
const newExclude = new Set(newConfig.env.exclude);
|
|
181
|
+
if (oldExclude.size === newExclude.size && oldExclude.isSubsetOf(newExclude)) {
|
|
182
|
+
// `env.exclude` has not changed. Skip.
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
logger.info('[FileWatcher] env.exclude config has changed. Reconciling...');
|
|
186
|
+
for (const root of projectRoots) {
|
|
187
|
+
await fileWatcher.reconcile(root);
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
await service.project.ready({
|
|
192
|
+
projectRootsWatcher: fileWatcher,
|
|
193
|
+
});
|
|
146
194
|
if (capabilities.workspace?.workspaceFolders) {
|
|
147
195
|
connection.workspace.onDidChangeWorkspaceFolders(async () => {
|
|
148
196
|
// FIXME
|
|
@@ -164,7 +212,7 @@ function startDynamicSemanticTokensRegistration() {
|
|
|
164
212
|
return;
|
|
165
213
|
}
|
|
166
214
|
logger.info('[registerDynamicSemanticTokens] Registering dynamic semantic tokens');
|
|
167
|
-
dynamicSemanticTokensDiposable = connection.client.register(ls.SemanticTokensRegistrationType.type, buildSemanticTokensCapability());
|
|
215
|
+
dynamicSemanticTokensDiposable = connection.client.register(ls.SemanticTokensRegistrationType.type, buildSemanticTokensCapability(true));
|
|
168
216
|
}
|
|
169
217
|
function unregisterDynamicSemanticTokens() {
|
|
170
218
|
logger.info('[unregisterDynamicSemanticTokens] Unregistering dynamic semantic tokens');
|
|
@@ -174,13 +222,39 @@ function startDynamicSemanticTokensRegistration() {
|
|
|
174
222
|
if (service.project.config.env.feature.semanticColoring) {
|
|
175
223
|
registerDynamicSemanticTokens();
|
|
176
224
|
}
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
225
|
+
function didConfigChange(oldSemanticTokensConfig, newSemanticTokensConfig) {
|
|
226
|
+
if (oldSemanticTokensConfig === newSemanticTokensConfig) {
|
|
227
|
+
return false;
|
|
180
228
|
}
|
|
181
|
-
|
|
229
|
+
if (typeof oldSemanticTokensConfig !== 'object'
|
|
230
|
+
|| typeof newSemanticTokensConfig !== 'object') {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (!oldSemanticTokensConfig.disabledLanguages
|
|
234
|
+
&& !newSemanticTokensConfig.disabledLanguages) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(oldSemanticTokensConfig.disabledLanguages)
|
|
238
|
+
&& Array.isArray(newSemanticTokensConfig.disabledLanguages)
|
|
239
|
+
&& oldSemanticTokensConfig.disabledLanguages.length
|
|
240
|
+
=== newSemanticTokensConfig.disabledLanguages.length
|
|
241
|
+
&& oldSemanticTokensConfig.disabledLanguages.every((language, index) => language === newSemanticTokensConfig.disabledLanguages[index])) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
service.project.on('configChanged', ({ oldConfig, newConfig }) => {
|
|
247
|
+
const oldSemanticTokensConfig = oldConfig.env.feature.semanticColoring;
|
|
248
|
+
const newSemanticTokensConfig = newConfig.env.feature.semanticColoring;
|
|
249
|
+
if (!didConfigChange(oldSemanticTokensConfig, newSemanticTokensConfig)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (oldSemanticTokensConfig) {
|
|
182
253
|
unregisterDynamicSemanticTokens();
|
|
183
254
|
}
|
|
255
|
+
if (newSemanticTokensConfig) {
|
|
256
|
+
registerDynamicSemanticTokens();
|
|
257
|
+
}
|
|
184
258
|
});
|
|
185
259
|
}
|
|
186
260
|
connection.onDidOpenTextDocument(({ textDocument: { text, uri, version, languageId: languageID } }) => {
|
|
@@ -192,7 +266,6 @@ connection.onDidChangeTextDocument(({ contentChanges, textDocument: { uri, versi
|
|
|
192
266
|
connection.onDidCloseTextDocument(({ textDocument: { uri } }) => {
|
|
193
267
|
service.project.onDidClose(uri);
|
|
194
268
|
});
|
|
195
|
-
connection.workspace.onDidRenameFiles(({}) => { });
|
|
196
269
|
connection.onCodeAction(async ({ textDocument: { uri }, range }) => {
|
|
197
270
|
const docAndNode = await service.project.ensureClientManagedChecked(uri);
|
|
198
271
|
if (!docAndNode || !service.project.config.env.feature.codeActions) {
|
|
@@ -378,6 +451,12 @@ connection.onDocumentFormatting(async ({ textDocument: { uri }, options }) => {
|
|
|
378
451
|
}
|
|
379
452
|
return [toLS.textEdit(node.range, text, doc)];
|
|
380
453
|
});
|
|
454
|
+
connection.onDidChangeConfiguration(updateEditorConfiguration);
|
|
455
|
+
async function updateEditorConfiguration() {
|
|
456
|
+
const settings = await connection.workspace.getConfiguration({ section: 'spyglassmc' });
|
|
457
|
+
const config = core.PartialConfig.buildConfigFromEditorSettingsSafe(settings);
|
|
458
|
+
await service.project.onEditorConfigurationUpdate(config);
|
|
459
|
+
}
|
|
381
460
|
connection.onShutdown(async () => {
|
|
382
461
|
await service.project.close();
|
|
383
462
|
hasShutdown = true;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as core from '@spyglassmc/core';
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
import * as ls from 'vscode-languageserver/node.js';
|
|
4
|
+
type Predicate = (uri: string) => boolean;
|
|
5
|
+
export interface LspFileWatcherOptions {
|
|
6
|
+
capabilities: ls.ClientCapabilities;
|
|
7
|
+
connection: ls.Connection;
|
|
8
|
+
externals: core.Externals;
|
|
9
|
+
locations: readonly core.FsLocation[];
|
|
10
|
+
logger: core.Logger;
|
|
11
|
+
predicate: Predicate;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A file watcher based on Language Server Protocol's `workspace/didChangeWatchedFiles`
|
|
15
|
+
* notification.
|
|
16
|
+
*/
|
|
17
|
+
export declare class LspFileWatcher extends EventEmitter implements core.FileWatcher {
|
|
18
|
+
#private;
|
|
19
|
+
get watchedFiles(): core.UriStore;
|
|
20
|
+
constructor({ capabilities, connection, externals, locations, logger, predicate }: LspFileWatcherOptions);
|
|
21
|
+
ready(): Promise<void>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Reconcile the internal URI store with the actual directories and files on the disk.
|
|
25
|
+
* @param uri URI that should be reconciled. If it's a file URI, ensure the file exists in the
|
|
26
|
+
* internal URI store; if it's a directory URI, ensure all contents of it exists and no extra
|
|
27
|
+
* content is recorded; if the URI does not exist, reconcile its parent URI up until the watched
|
|
28
|
+
* `locations`.
|
|
29
|
+
*/
|
|
30
|
+
reconcile(uri: core.FsLocation): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=LspFileWatcher.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as core from '@spyglassmc/core';
|
|
2
|
+
import EventEmitter from 'events';
|
|
3
|
+
import * as ls from 'vscode-languageserver/node.js';
|
|
4
|
+
/**
|
|
5
|
+
* A file watcher based on Language Server Protocol's `workspace/didChangeWatchedFiles`
|
|
6
|
+
* notification.
|
|
7
|
+
*/
|
|
8
|
+
export class LspFileWatcher extends EventEmitter {
|
|
9
|
+
#ready = false;
|
|
10
|
+
#connection;
|
|
11
|
+
#externals;
|
|
12
|
+
#locations;
|
|
13
|
+
#logger;
|
|
14
|
+
#predicate;
|
|
15
|
+
#watchedFiles = new core.UriStore();
|
|
16
|
+
#lspDisposables = [];
|
|
17
|
+
get watchedFiles() {
|
|
18
|
+
return this.#watchedFiles;
|
|
19
|
+
}
|
|
20
|
+
constructor({ capabilities, connection, externals, locations, logger, predicate }) {
|
|
21
|
+
super();
|
|
22
|
+
this.#connection = connection;
|
|
23
|
+
this.#externals = externals;
|
|
24
|
+
this.#locations = locations.map((uri) => core.normalizeUri(uri.toString()));
|
|
25
|
+
this.#logger = logger;
|
|
26
|
+
this.#predicate = predicate;
|
|
27
|
+
if (!capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration) {
|
|
28
|
+
throw new Error('LspFileWatcher requires dynamic registration capability for didChangeWatchedFiles notifications');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async ready() {
|
|
32
|
+
try {
|
|
33
|
+
this.#lspDisposables = [
|
|
34
|
+
this.#connection.onDidChangeWatchedFiles((params) => {
|
|
35
|
+
this.#logger.info('[LspFileWatcher] raw LSP changes', JSON.stringify(params));
|
|
36
|
+
return this.#onLspDidChangeWatchedFiles(params);
|
|
37
|
+
}),
|
|
38
|
+
await this.#connection.client.register(ls.DidChangeWatchedFilesNotification.type, {
|
|
39
|
+
// "**/*" is needed to watch changes to folders as well.
|
|
40
|
+
// https://github.com/microsoft/vscode/issues/60813#issuecomment-1145821690
|
|
41
|
+
watchers: [{ globPattern: '**/*' }],
|
|
42
|
+
}),
|
|
43
|
+
];
|
|
44
|
+
for (const location of this.#locations) {
|
|
45
|
+
for (const uri of await core.fileUtil.getAllFiles(this.#externals, location)) {
|
|
46
|
+
if (this.#predicate(uri)) {
|
|
47
|
+
this.#watchedFiles.add(uri);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
this.#ready = true;
|
|
52
|
+
this.emit('ready');
|
|
53
|
+
}
|
|
54
|
+
catch (e) {
|
|
55
|
+
this.emit('error', e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async close() {
|
|
59
|
+
for (const disposable of this.#lspDisposables) {
|
|
60
|
+
disposable.dispose();
|
|
61
|
+
}
|
|
62
|
+
this.#lspDisposables = [];
|
|
63
|
+
}
|
|
64
|
+
async #onLspDidChangeWatchedFiles({ changes }) {
|
|
65
|
+
if (!this.#ready) {
|
|
66
|
+
throw new Error('Callback #onLspDidChangeWatchedFiles executed before ready');
|
|
67
|
+
}
|
|
68
|
+
for (const { type, uri } of changes) {
|
|
69
|
+
try {
|
|
70
|
+
const normalizedUri = core.normalizeUri(uri);
|
|
71
|
+
switch (type) {
|
|
72
|
+
case ls.FileChangeType.Created: {
|
|
73
|
+
await this.#handleAdd(core.fileUtil.trimEndingSlash(normalizedUri));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case ls.FileChangeType.Changed:
|
|
77
|
+
await this.#handleChange(core.fileUtil.trimEndingSlash(normalizedUri));
|
|
78
|
+
break;
|
|
79
|
+
case ls.FileChangeType.Deleted: {
|
|
80
|
+
this.#handleDelete(core.fileUtil.trimEndingSlash(normalizedUri));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
this.#logger.error('[LspFileWatcher] handle error', e);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async #handleAdd(uri) {
|
|
91
|
+
let stat;
|
|
92
|
+
try {
|
|
93
|
+
stat = await this.#externals.fs.stat(uri);
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
if (!this.#externals.error.isKind(e, 'ENOENT')) {
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
// LSP gave us a non-existent URI. Reconcile its parent directory just to be safe.
|
|
100
|
+
this.#logger.warn(`[LspFileWatcher] non-existent URI ${uri}; will reconcile parent directory`);
|
|
101
|
+
return this.#reconcileParentOf(uri);
|
|
102
|
+
}
|
|
103
|
+
if (stat.isDirectory()) {
|
|
104
|
+
// Find all files under the added URI and send 'add' events for them.
|
|
105
|
+
for (const fileUri of await core.fileUtil.getAllFiles(this.#externals, uri)) {
|
|
106
|
+
this.#addIfNeeded(fileUri);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (stat.isFile()) {
|
|
110
|
+
this.#addIfNeeded(uri);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async #handleChange(uri) {
|
|
114
|
+
if (!this.#predicate(uri) || this.#watchedFiles.has(core.fileUtil.ensureEndingSlash(uri))) {
|
|
115
|
+
// Skip non-predicate matching URIs and directory URIs.
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (!this.#watchedFiles.has(uri)) {
|
|
119
|
+
this.#logger.warn(`[LspFileWatcher] unknown changed URI ${uri}; handling as add instead`);
|
|
120
|
+
return this.#handleAdd(uri);
|
|
121
|
+
}
|
|
122
|
+
this.emit('change', uri);
|
|
123
|
+
}
|
|
124
|
+
#handleDelete(uri) {
|
|
125
|
+
const dirUri = core.fileUtil.ensureEndingSlash(uri);
|
|
126
|
+
if (this.#watchedFiles.has(uri)) {
|
|
127
|
+
// Is a file URI. Delete it directly.
|
|
128
|
+
this.#watchedFiles.delete(uri);
|
|
129
|
+
this.emit('unlink', uri);
|
|
130
|
+
}
|
|
131
|
+
else if (this.#watchedFiles.has(dirUri)) {
|
|
132
|
+
// Is a directory URI.
|
|
133
|
+
// Find all files under the deleted URI and send 'unlink' events for them.
|
|
134
|
+
// getSubFiles() returns an iterator that would return nothing after dirUri
|
|
135
|
+
// is deleted from #watchedFiles, therefore we need to collect the results of
|
|
136
|
+
// the iterator before it is deleted.
|
|
137
|
+
const subFiles = [...this.#watchedFiles.getSubFiles(dirUri)];
|
|
138
|
+
this.#watchedFiles.delete(dirUri);
|
|
139
|
+
for (const watchedUri of subFiles) {
|
|
140
|
+
this.emit('unlink', watchedUri);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Add the file URI to the internal store and emit the `add` event if the file hasn't been
|
|
146
|
+
* excluded by predicate and doesn't already exist in the store.
|
|
147
|
+
*/
|
|
148
|
+
#addIfNeeded(uri) {
|
|
149
|
+
if (!this.#watchedFiles.has(uri) && this.#predicate(uri)) {
|
|
150
|
+
this.#watchedFiles.add(uri);
|
|
151
|
+
this.emit('add', uri);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Delete the file URI from the internal store and emit the `unlink` event if the file has been
|
|
156
|
+
* excluded by predicate but exist in the store.
|
|
157
|
+
*/
|
|
158
|
+
#deleteIfExcluded(uri) {
|
|
159
|
+
if (this.#watchedFiles.has(uri) && !this.#predicate(uri)) {
|
|
160
|
+
this.#watchedFiles.delete(uri);
|
|
161
|
+
this.emit('unlink', uri);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Reconcile the internal URI store with the actual directories and files on the disk.
|
|
166
|
+
* @param uri URI that should be reconciled. If it's a file URI, ensure the file exists in the
|
|
167
|
+
* internal URI store; if it's a directory URI, ensure all contents of it exists and no extra
|
|
168
|
+
* content is recorded; if the URI does not exist, reconcile its parent URI up until the watched
|
|
169
|
+
* `locations`.
|
|
170
|
+
*/
|
|
171
|
+
async reconcile(uri) {
|
|
172
|
+
return this.#reconcile(core.normalizeUri(uri.toString()));
|
|
173
|
+
}
|
|
174
|
+
async #reconcile(uri, stat) {
|
|
175
|
+
uri = core.fileUtil.trimEndingSlash(uri);
|
|
176
|
+
try {
|
|
177
|
+
stat ??= await this.#externals.fs.stat(uri);
|
|
178
|
+
if (stat.isDirectory()) {
|
|
179
|
+
// For directory, reconcile all its entries recursively.
|
|
180
|
+
const dirUri = core.fileUtil.ensureEndingSlash(uri);
|
|
181
|
+
const diskEntries = await this.#externals.fs.readdir(dirUri);
|
|
182
|
+
const diskEntryNames = new Set();
|
|
183
|
+
for (const diskEntry of diskEntries) {
|
|
184
|
+
diskEntryNames.add(diskEntry.name);
|
|
185
|
+
await this.#reconcile(core.fileUtil.join(dirUri, diskEntry.name), diskEntry);
|
|
186
|
+
}
|
|
187
|
+
// Remove extra entries of this directory, if any, from the internal URI store.
|
|
188
|
+
const storeEntryNames = this.#watchedFiles.getChildrenNames(dirUri);
|
|
189
|
+
for (const storeEntryName of storeEntryNames) {
|
|
190
|
+
if (!diskEntryNames.has(storeEntryName)) {
|
|
191
|
+
this.#handleDelete(core.fileUtil.join(dirUri, storeEntryName));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (stat.isFile()) {
|
|
196
|
+
// For file, ensure it exists in the internal store if it belongs there.
|
|
197
|
+
this.#addIfNeeded(uri);
|
|
198
|
+
// And delete it if it should be excluded.
|
|
199
|
+
this.#deleteIfExcluded(uri);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
if (!this.#externals.error.isKind(e, 'ENOENT')) {
|
|
204
|
+
throw e;
|
|
205
|
+
}
|
|
206
|
+
// The URI does not exist. Remove it from internal store.
|
|
207
|
+
this.#logger.warn(`[LspFileWatcher] non-existent URI during reconcilation ${uri}; will reconcile with further parent directory`);
|
|
208
|
+
this.#handleDelete(uri);
|
|
209
|
+
// It is weird that reconcile() was called on a non-existent URI. We will go up a
|
|
210
|
+
// directory and reconcile there as well just to fix anything that might be wrong.
|
|
211
|
+
return this.#reconcileParentOf(uri);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async #reconcileParentOf(uri) {
|
|
215
|
+
const parentUri = core.fileUtil.trimEndingSlash(core.fileUtil.getParentOfUri(uri).toString());
|
|
216
|
+
// We only reconcile the parent if it is different from the current URI (to avoid an infinite
|
|
217
|
+
// loop) and if it is under the watched locations of the file watcher.
|
|
218
|
+
if (!(parentUri !== uri
|
|
219
|
+
&& this.#locations.some((loc) => core.fileUtil.isSubUriOf(parentUri, loc.toString())))) {
|
|
220
|
+
this.#logger.warn(`[LspFileWatcher] reconcilation stopped after ${uri} at ${parentUri}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
return this.reconcile(parentUri);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=LspFileWatcher.js.map
|
package/lib/util/toLS.d.ts
CHANGED
|
@@ -10,7 +10,9 @@ export declare function diagnostic(error: core.PosRangeLanguageError): ls.Diagno
|
|
|
10
10
|
export declare function diagnostics(errors: readonly core.PosRangeLanguageError[]): ls.Diagnostic[];
|
|
11
11
|
export declare function diagnosticSeverity(severity: core.ErrorSeverity): ls.DiagnosticSeverity;
|
|
12
12
|
export declare function documentHighlight(locations: core.SymbolLocations | undefined): ls.DocumentHighlight[] | undefined;
|
|
13
|
-
export declare function documentSelector(meta: core.MetaRegistry
|
|
13
|
+
export declare function documentSelector(meta: core.MetaRegistry, { disabledLanguages }?: {
|
|
14
|
+
disabledLanguages?: string[];
|
|
15
|
+
}): ls.DocumentSelector;
|
|
14
16
|
export declare function documentSymbol(symbol: core.Symbol, symLoc: core.SymbolLocation, doc: TextDocument, hierarchicalSupport: boolean | undefined, supportedKinds?: ls.SymbolKind[]): ls.DocumentSymbol;
|
|
15
17
|
export declare function documentSymbols(map: core.SymbolMap | undefined, doc: TextDocument, hierarchicalSupport: boolean | undefined, supportedKinds?: ls.SymbolKind[]): ls.DocumentSymbol[];
|
|
16
18
|
export declare function documentSymbolsFromTable(table: core.SymbolTable, doc: TextDocument, hierarchicalSupport: boolean | undefined, supportedKinds?: ls.SymbolKind[]): ls.DocumentSymbol[];
|
package/lib/util/toLS.js
CHANGED
|
@@ -65,8 +65,8 @@ export function documentHighlight(locations) {
|
|
|
65
65
|
range: loc.posRange,
|
|
66
66
|
}));
|
|
67
67
|
}
|
|
68
|
-
export function documentSelector(meta) {
|
|
69
|
-
const ans = meta.getLanguages().map((id) => ({ language: id }));
|
|
68
|
+
export function documentSelector(meta, { disabledLanguages } = {}) {
|
|
69
|
+
const ans = meta.getLanguages().filter((id) => disabledLanguages === undefined || !disabledLanguages.includes(id)).map((id) => ({ language: id }));
|
|
70
70
|
return ans;
|
|
71
71
|
}
|
|
72
72
|
export function documentSymbol(symbol, symLoc, doc, hierarchicalSupport, supportedKinds = []) {
|
package/lib/util/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { PartialConfig } from '@spyglassmc/core';
|
|
1
2
|
export interface CustomInitializationOptions {
|
|
2
3
|
inDevelopmentMode?: boolean;
|
|
3
|
-
|
|
4
|
+
defaultConfig?: PartialConfig;
|
|
4
5
|
}
|
|
5
6
|
export interface CustomServerCapabilities {
|
|
6
7
|
dataHackPubify?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spyglassmc/language-server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.57",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "lib/server.js",
|
|
6
6
|
"types": "lib/server.d.ts",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"env-paths": "^2.2.1",
|
|
36
36
|
"vscode-languageserver": "^9.0.1",
|
|
37
37
|
"vscode-languageserver-textdocument": "^1.0.11",
|
|
38
|
-
"@spyglassmc/core": "0.4.
|
|
39
|
-
"@spyglassmc/java-edition": "0.3.
|
|
40
|
-
"@spyglassmc/locales": "0.3.
|
|
41
|
-
"@spyglassmc/mcdoc": "0.3.
|
|
38
|
+
"@spyglassmc/core": "0.4.45",
|
|
39
|
+
"@spyglassmc/java-edition": "0.3.57",
|
|
40
|
+
"@spyglassmc/locales": "0.3.23",
|
|
41
|
+
"@spyglassmc/mcdoc": "0.3.49"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|