-
-
Notifications
You must be signed in to change notification settings - Fork 199
Expand file tree
/
Copy pathJsonLsp.js
More file actions
266 lines (248 loc) · 10.9 KB
/
Copy pathJsonLsp.js
File metadata and controls
266 lines (248 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/
/**
* JsonLsp - boots the vscode-json-language-server for schema-aware JSON intelligence:
* completion for known config files (package.json fields, tsconfig options, ...), hover docs
* from schema descriptions, and validation diagnostics in the Problems panel.
*
* Mirrors TypeScriptSupport's lazy-start model: the server is only spawned once a JSON file is
* the active editor, and a project switch repoints the running server instead of restarting it.
* Desktop-only (the server is a node process). After every server (re)start the curated schema
* associations are pushed via workspace/didChangeConfiguration - the server itself downloads and
* caches the schemas over http(s).
*
* @module extensionsIntegrated/JSONSupport/JsonLsp
*/
define(function (require, exports, module) {
const EditorManager = require("editor/EditorManager"),
ProjectManager = require("project/ProjectManager"),
NodeConnector = require("NodeConnector"),
PreferencesManager = require("preferences/PreferencesManager"),
Strings = require("strings"),
SchemaAssociations = require("./schemaAssociations");
const SERVER_ID = "json";
const SUPPORTED_LANGUAGES = ["json"];
const PREF_JSON_CODE_INTELLIGENCE = "codeIntelligence." + SERVER_ID;
// Master switch for JSON code intelligence - setting it back to true starts the server on
// the open JSON file. A runtime disable of a running server is handled centrally by
// LSPClient's own watcher on this pref.
PreferencesManager.definePreference(PREF_JSON_CODE_INTELLIGENCE, "boolean", true, {
description: Strings.DESCRIPTION_JSON_CODE_INTELLIGENCE
});
let lspClientPromise = null;
let client = null; // the LanguageClient once registered
let registered = false;
let starting = false;
let pendingRepoint = false;
let initErrorReported = false;
// Test hook: when set, pushed instead of the curated table (lets integration tests use a
// file:// schema so schema features are verifiable without internet access).
let _schemaAssociationsOverride = null;
/**
* LSP only runs in the desktop app where the Node engine is available.
* @return {boolean}
*/
function canRun() {
return Phoenix.isNativeApp && NodeConnector.isNodeAvailable();
}
// Lazy-load the LSP framework so it stays out of the boot dependency graph. Memoized; retries
// once to ride out any module-load race during startup (same pattern as TypeScriptSupport).
function loadLSPClient() {
if (!lspClientPromise) {
lspClientPromise = new Promise(function (resolve, reject) {
require(["languageTools/LSPClient"], resolve, function () {
setTimeout(function () {
require(["languageTools/LSPClient"], resolve, reject);
}, 500);
});
});
}
return lspClientPromise;
}
function waitForNodeReady(timeout) {
return new Promise(function (resolve) {
const deadline = Date.now() + timeout;
(function check() {
if (NodeConnector.isNodeReady()) {
resolve(true);
} else if (Date.now() > deadline) {
resolve(false);
} else {
setTimeout(check, 300);
}
}());
});
}
/**
* Push the JSON server's settings: validation on, formatting off (Phoenix has its own
* formatters) and the schema-association table. Sent after EVERY server up-transition -
* initial start, manual/project restart, and crash auto-restart - since a fresh server
* process starts with no configuration.
*/
function _pushConfiguration() {
if (!client) {
return;
}
client.sendCustomNotification("workspace/didChangeConfiguration", {
settings: {
json: {
validate: { enable: true },
format: { enable: false },
schemas: _schemaAssociationsOverride || SchemaAssociations.SCHEMA_ASSOCIATIONS
}
}
});
}
async function start() {
if (registered || !canRun()) {
return;
}
const ready = await waitForNodeReady(30000);
if (!ready) {
console.error("[JSONSupport] Node not ready - JSON LSP disabled");
return;
}
const LSPClient = await loadLSPClient();
// Re-push configuration on every later up-transition (manual restart, crash auto-restart)
// - a fresh server process starts with no settings. The INITIAL start is covered by the
// direct push below instead: this event fires inside registerLanguageServer, before its
// result is assigned to `client`, so the handler would see client === null and skip.
LSPClient.on(LSPClient.EVENT_LANGUAGE_SERVER_STARTED + ".jsonSupport", function (_evt, data) {
if (data.serverId === SERVER_ID) {
_pushConfiguration();
}
});
client = await LSPClient.registerLanguageServer({
serverId: SERVER_ID,
command: "vscode-json-language-server",
args: ["--stdio"],
languages: SUPPORTED_LANGUAGES,
// The server is comment-tolerant in jsonc mode. Real-world tsconfig/.eslintrc commonly
// carry comments, so serve ALL json documents as jsonc - package.json with comments is
// invalid for npm, but schema validation still applies and the npm CLI reports that
// case better than a squiggly storm would.
languageIdMap: { json: "jsonc" },
initializationOptions: {
provideFormatter: false
// handledSchemaProtocols deliberately unset: the server then fetches http/https
// schema URLs itself (NodeJS http) - no client-side schema proxying needed.
},
// The JSON server refuses to offer completion unless the client supports snippets
// (its completions insert `"key": $1` templates). Our insertHint expands snippets via
// TabstopManager, so this is safe to advertise for this server.
completionSnippetSupport: true,
// Yield Phoenix preference files to PrefsCodeHints (priority 0) - it hints actual
// preference keys/values there, which beats generic schema completion.
documentFilter: function (fullPath) {
const name = fullPath.substring(fullPath.lastIndexOf("/") + 1);
return !/^\.?(brackets|phcode)\.json$/.test(name);
}
});
if (client) {
registered = true;
_pushConfiguration();
}
}
function _isServedLanguageActive() {
const editor = EditorManager.getActiveEditor();
if (!editor) {
return false;
}
return SUPPORTED_LANGUAGES.indexOf(editor.getLanguageForSelection().getId()) !== -1;
}
/**
* Lazily start the server when a JSON file is active; repoint (not restart) the running server
* after a project switch. Same onLanguage model as TypeScriptSupport - projects that never
* open a JSON file never spawn the server.
*/
function _ensureServerForActiveEditor() {
if (!canRun() || !_isServedLanguageActive()) {
return;
}
if (PreferencesManager.get(PREF_JSON_CODE_INTELLIGENCE) === false) {
return;
}
if (!registered) {
if (starting) {
return;
}
starting = true;
pendingRepoint = false;
start().catch(function (err) {
if (!initErrorReported) {
initErrorReported = true;
window.logger && window.logger.reportError(err, "[JSONSupport] JSON LSP init failed");
}
}).finally(function () {
starting = false;
});
return;
}
if (pendingRepoint) {
pendingRepoint = false;
loadLSPClient().then(function (LSPClient) {
LSPClient.changeWorkspaceRoot(SERVER_ID);
});
}
}
/**
* Wire the lazy-start lifecycle. Call from appReady; no-ops outside the desktop app.
*/
function init() {
if (!canRun()) {
return;
}
loadLSPClient(); // pre-warm the framework module (not the server)
EditorManager.on("activeEditorChange.jsonSupport", _ensureServerForActiveEditor);
_ensureServerForActiveEditor();
ProjectManager.on(ProjectManager.EVENT_PROJECT_OPEN + ".jsonSupport", function () {
pendingRepoint = true;
_ensureServerForActiveEditor();
});
// Re-enabling the pref starts the server on the open JSON file without needing a file
// switch. The disable direction is handled by LSPClient stopping the running server.
PreferencesManager.on("change", PREF_JSON_CODE_INTELLIGENCE, function () {
if (PreferencesManager.get(PREF_JSON_CODE_INTELLIGENCE) !== false) {
_ensureServerForActiveEditor();
}
});
}
/**
* Test hook - override the schema associations (e.g. with a file:// fixture schema) and
* re-push to the running server. Pass null to restore the curated table.
* @param {?Array<{fileMatch: string[], url: string}>} associations
*/
function _setTestSchemaAssociations(associations) {
_schemaAssociationsOverride = associations;
_pushConfiguration();
}
exports.init = init;
exports.canRun = canRun;
exports.SERVER_ID = SERVER_ID;
exports._setTestSchemaAssociations = _setTestSchemaAssociations;
if (Phoenix.isTestWindow) {
// Test hook - the registered LanguageClient (null until the server has started). Lets
// integration tests drive the LSP providers (e.g. codeHints) directly, since the hint UI
// needs OS window focus that the embedded test window may not have.
exports._getClient = function () {
return client;
};
}
});