31
31
* @typedef {EngineFields & LanguageServerFields } Options
32
32
*/
33
33
34
- import { PassThrough } from 'node:stream '
34
+ import path from 'node:path '
35
35
import process from 'node:process'
36
- import { URL , pathToFileURL } from 'node:url'
36
+ import { PassThrough } from 'node:stream'
37
+ import { URL , pathToFileURL , fileURLToPath } from 'node:url'
37
38
38
39
import { loadPlugin } from 'load-plugin'
39
40
import { engine } from 'unified-engine'
@@ -137,12 +138,7 @@ function vfileMessageToDiagnostic(message) {
137
138
* @returns {VFile }
138
139
*/
139
140
function lspDocumentToVfile ( document ) {
140
- return new VFile ( {
141
- // VFile expects a file path or file URL object, but LSP provides a file URI
142
- // as a string.
143
- path : new URL ( document . uri ) ,
144
- value : document . getText ( )
145
- } )
141
+ return new VFile ( { path : new URL ( document . uri ) , value : document . getText ( ) } )
146
142
}
147
143
148
144
/**
@@ -164,6 +160,9 @@ export function configureUnifiedLanguageServer(
164
160
rcName
165
161
}
166
162
) {
163
+ /** @type {Set<string> } */
164
+ const workspaces = new Set ( )
165
+
167
166
/**
168
167
* Process various LSP text documents using unified and send back the
169
168
* resulting messages as diagnostics.
@@ -173,76 +172,119 @@ export function configureUnifiedLanguageServer(
173
172
* @returns {Promise<VFile[]> }
174
173
*/
175
174
async function processDocuments ( textDocuments , alwaysStringify = false ) {
176
- /** @type {EngineOptions['processor'] } */
177
- let processor
178
-
179
- try {
180
- // @ts -expect-error: assume we load a unified processor.
181
- processor = await loadPlugin ( processorName , {
182
- cwd : process . cwd ( ) ,
183
- key : processorSpecifier
184
- } )
185
- } catch ( error ) {
186
- const exception = /** @type {NodeJS.ErrnoException } */ ( error )
187
-
188
- // Pass other funky errors through.
189
- /* c8 ignore next 3 */
190
- if ( exception . code !== 'ERR_MODULE_NOT_FOUND' ) {
191
- throw error
192
- }
193
-
194
- if ( ! defaultProcessor ) {
195
- connection . window . showInformationMessage (
196
- 'Cannot turn on language server without `' +
197
- processorName +
198
- '` locally. Run `npm install ' +
199
- processorName +
200
- '` to enable it'
201
- )
202
- return [ ]
203
- }
204
-
205
- const problem = new Error (
206
- 'Cannot find `' +
207
- processorName +
208
- '` locally but using `defaultProcessor`, original error:\n' +
209
- exception . stack
210
- )
175
+ // LSP uses `file:` URLs (hrefs), `unified-engine` expects a paths.
176
+ // `process.cwd()` does not add a final slash, but `file:` URLs often do.
177
+ const workspacesAsPaths = [ ...workspaces ] . map ( ( d ) =>
178
+ fileURLToPath ( d . replace ( / \/ $ / , '' ) )
179
+ )
180
+ /** @type {Map<string, Array<VFile>> } */
181
+ const workspacePathToFiles = new Map ( )
211
182
212
- connection . console . log ( String ( problem ) )
183
+ if ( workspacesAsPaths . length === 0 ) {
184
+ workspacesAsPaths . push ( process . cwd ( ) )
185
+ }
213
186
214
- processor = defaultProcessor
187
+ for ( const textDocument of textDocuments ) {
188
+ const file = lspDocumentToVfile ( textDocument )
189
+ const [ cwd ] = workspacesAsPaths
190
+ // Every workspace that includes the document.
191
+ . filter ( ( d ) => file . path . slice ( 0 , d . length + 1 ) === d + path . sep )
192
+ // Sort the longest (closest to the file) first.
193
+ . sort ( ( a , b ) => b . length - a . length )
194
+
195
+ // This presumably should not occur: a file outside a workspace.
196
+ // So ignore the file.
197
+ /* c8 ignore next */
198
+ if ( ! cwd ) continue
199
+
200
+ const files = workspacePathToFiles . get ( cwd ) || [ ]
201
+ workspacePathToFiles . set ( cwd , [ ...files , file ] )
215
202
}
216
203
217
- return new Promise ( ( resolve , reject ) => {
218
- engine (
219
- {
220
- alwaysStringify,
221
- files : textDocuments . map ( ( document ) => lspDocumentToVfile ( document ) ) ,
222
- ignoreName,
223
- packageField,
224
- pluginPrefix,
225
- plugins,
226
- processor,
227
- quiet : false ,
228
- rcName,
229
- silentlyIgnore : true ,
230
- streamError : new PassThrough ( ) ,
231
- streamOut : new PassThrough ( )
232
- } ,
233
- ( error , _ , context ) => {
234
- // An error never occur and can’t be reproduced. Thus us ab internal
235
- // error in unified-engine. If a plugin throws, it’s reported as a
236
- // vfile message.
237
- /* c8 ignore start */
238
- if ( error ) {
239
- reject ( error )
240
- } else {
241
- resolve ( ( context && context . files ) || [ ] )
204
+ /** @type {Array<Promise<Array<VFile>>> } */
205
+ const promises = [ ]
206
+
207
+ for ( const [ cwd , files ] of workspacePathToFiles ) {
208
+ promises . push (
209
+ ( async function ( ) {
210
+ /** @type {EngineOptions['processor'] } */
211
+ let processor
212
+
213
+ try {
214
+ // @ts -expect-error: assume we load a unified processor.
215
+ processor = await loadPlugin ( processorName , {
216
+ cwd,
217
+ key : processorSpecifier
218
+ } )
219
+ } catch ( error ) {
220
+ const exception = /** @type {NodeJS.ErrnoException } */ ( error )
221
+
222
+ // Pass other funky errors through.
223
+ /* c8 ignore next 3 */
224
+ if ( exception . code !== 'ERR_MODULE_NOT_FOUND' ) {
225
+ throw error
226
+ }
227
+
228
+ if ( ! defaultProcessor ) {
229
+ connection . window . showInformationMessage (
230
+ 'Cannot turn on language server without `' +
231
+ processorName +
232
+ '` locally. Run `npm install ' +
233
+ processorName +
234
+ '` to enable it'
235
+ )
236
+ return [ ]
237
+ }
238
+
239
+ connection . console . log (
240
+ 'Cannot find `' +
241
+ processorName +
242
+ '` locally but using `defaultProcessor`, original error:\n' +
243
+ exception . stack
244
+ )
245
+
246
+ processor = defaultProcessor
242
247
}
243
- }
248
+
249
+ return new Promise ( ( resolve , reject ) => {
250
+ engine (
251
+ {
252
+ alwaysStringify,
253
+ cwd,
254
+ files,
255
+ ignoreName,
256
+ packageField,
257
+ pluginPrefix,
258
+ plugins,
259
+ processor,
260
+ quiet : false ,
261
+ rcName,
262
+ silentlyIgnore : true ,
263
+ streamError : new PassThrough ( ) ,
264
+ streamOut : new PassThrough ( )
265
+ } ,
266
+ ( error , _ , context ) => {
267
+ // An error never occured and can’t be reproduced. This is an internal
268
+ // error in unified-engine. If a plugin throws, it’s reported as a
269
+ // vfile message.
270
+ /* c8 ignore start */
271
+ if ( error ) {
272
+ reject ( error )
273
+ } else {
274
+ resolve ( ( context && context . files ) || [ ] )
275
+ }
276
+ }
277
+ )
278
+ } )
279
+ } ) ( )
280
+ /* c8 ignore stop */
244
281
)
245
- } )
282
+ }
283
+
284
+ const listsOfFiles = await Promise . all ( promises )
285
+ // XXX [engine:node@>16] V8 coverage bug on Dubnium (Node 12).
286
+ /* c8 ignore next 3 */
287
+ return listsOfFiles . flat ( )
246
288
}
247
289
248
290
/* c8 ignore stop */
@@ -272,16 +314,48 @@ export function configureUnifiedLanguageServer(
272
314
}
273
315
}
274
316
275
- connection . onInitialize ( ( ) => ( {
276
- capabilities : {
277
- textDocumentSync : TextDocumentSyncKind . Full ,
278
- documentFormattingProvider : true ,
279
- codeActionProvider : {
280
- codeActionKinds : [ CodeActionKind . QuickFix ] ,
281
- resolveProvider : true
317
+ connection . onInitialize ( ( event ) => {
318
+ if ( event . workspaceFolders ) {
319
+ for ( const workspace of event . workspaceFolders ) {
320
+ workspaces . add ( workspace . uri )
282
321
}
283
322
}
284
- } ) )
323
+
324
+ if ( workspaces . size === 0 && event . rootUri ) {
325
+ workspaces . add ( event . rootUri )
326
+ }
327
+
328
+ if (
329
+ event . capabilities . workspace &&
330
+ event . capabilities . workspace . workspaceFolders
331
+ ) {
332
+ connection . workspace . onDidChangeWorkspaceFolders ( function ( event ) {
333
+ for ( const workspace of event . removed ) {
334
+ workspaces . delete ( workspace . uri )
335
+ }
336
+
337
+ for ( const workspace of event . added ) {
338
+ workspaces . add ( workspace . uri )
339
+ }
340
+
341
+ checkDocuments ( ...documents . all ( ) )
342
+ } )
343
+ }
344
+
345
+ return {
346
+ capabilities : {
347
+ textDocumentSync : TextDocumentSyncKind . Full ,
348
+ documentFormattingProvider : true ,
349
+ codeActionProvider : {
350
+ codeActionKinds : [ CodeActionKind . QuickFix ] ,
351
+ resolveProvider : true
352
+ } ,
353
+ workspace : {
354
+ workspaceFolders : { supported : true , changeNotifications : true }
355
+ }
356
+ }
357
+ }
358
+ } )
285
359
286
360
connection . onDocumentFormatting ( async ( event ) => {
287
361
const document = documents . get ( event . textDocument . uri )
0 commit comments