Skip to content

Commit 2859d30

Browse files
authored
Add support for workspaces
Closes GH-22. Reviewed-by: Remco Haszing <[email protected]>
1 parent 711e692 commit 2859d30

File tree

6 files changed

+508
-86
lines changed

6 files changed

+508
-86
lines changed

lib/index.js

Lines changed: 154 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@
3131
* @typedef {EngineFields & LanguageServerFields} Options
3232
*/
3333

34-
import {PassThrough} from 'node:stream'
34+
import path from 'node:path'
3535
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'
3738

3839
import {loadPlugin} from 'load-plugin'
3940
import {engine} from 'unified-engine'
@@ -137,12 +138,7 @@ function vfileMessageToDiagnostic(message) {
137138
* @returns {VFile}
138139
*/
139140
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()})
146142
}
147143

148144
/**
@@ -164,6 +160,9 @@ export function configureUnifiedLanguageServer(
164160
rcName
165161
}
166162
) {
163+
/** @type {Set<string>} */
164+
const workspaces = new Set()
165+
167166
/**
168167
* Process various LSP text documents using unified and send back the
169168
* resulting messages as diagnostics.
@@ -173,76 +172,119 @@ export function configureUnifiedLanguageServer(
173172
* @returns {Promise<VFile[]>}
174173
*/
175174
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()
211182

212-
connection.console.log(String(problem))
183+
if (workspacesAsPaths.length === 0) {
184+
workspacesAsPaths.push(process.cwd())
185+
}
213186

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])
215202
}
216203

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
242247
}
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 */
244281
)
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()
246288
}
247289

248290
/* c8 ignore stop */
@@ -272,16 +314,48 @@ export function configureUnifiedLanguageServer(
272314
}
273315
}
274316

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)
282321
}
283322
}
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+
})
285359

286360
connection.onDocumentFormatting(async (event) => {
287361
const document = documents.get(event.textDocument.uri)

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,5 +77,10 @@
7777
"plugins": [
7878
"remark-preset-wooorm"
7979
]
80+
},
81+
"typeCoverage": {
82+
"atLeast": 100,
83+
"detail": true,
84+
"strict": true
8085
}
8186
}

readme.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ Language servers created using this package implement the following language
177177
server features:
178178

179179
* `textDocument/codeAction`
180-
The language server implements code actions based on the `expected` field
180+
the language server implements code actions based on the `expected` field
181181
on reported messages.
182182
A code action can either insert, replace, or delete text based on the range
183183
of the message and the expected value.
@@ -199,9 +199,9 @@ server features:
199199
— when document formatting is requested by the client, the language server
200200
processes it using a unified pipeline.
201201
The stringified result is returned.
202-
* `workspace/didChangeWatchedFiles`
203-
When the client signals a watched file has changed, the language server
204-
processes all open files using a unified pipeline.
202+
* `workspace/didChangeWatchedFiles` and `workspace/didChangeWorkspaceFolders`
203+
when the client signals a watched file or workspace has changed, the
204+
language server processes all open files using a unified pipeline.
205205
Any messages collected are published to the client using
206206
`textDocument/publishDiagnostics`.
207207

@@ -213,7 +213,8 @@ As of now, that is Node.js 12.20+, 14.14+, and 16.0+.
213213
Our projects sometimes work with older versions, but this is not guaranteed.
214214

215215
This project uses [`vscode-languageserver`][vscode-languageserver] 7, which
216-
implements language server protocol 3.16.
216+
implements language server protocol 3.16.0.
217+
It should work anywhere where LSP 3.6.0 or later is implemented.
217218

218219
## Related
219220

test/folder/remark-with-cwd.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import {createUnifiedLanguageServer} from '../../index.js'
2+
3+
createUnifiedLanguageServer({
4+
processorName: 'remark',
5+
processorSpecifier: 'remark',
6+
plugins: [warn]
7+
})
8+
9+
/** @type {import('unified').Plugin<Array<void>>} */
10+
function warn() {
11+
return (_, file) => {
12+
file.message(file.cwd)
13+
}
14+
}

0 commit comments

Comments
 (0)