Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
coverage/
node_modules/
/test/.testremarkrc.json
*.log
*.d.ts
*.tgz
92 changes: 88 additions & 4 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,18 @@
* This can be used to ship a processor with your package, to be used if no
* processor is found locally.
* If this isn’t passed, a warning is shown if `processorName` can’t be found.
* @property {string} configurationSection
* This option will be used to give the client a hint of which configuration
* section to use.
* For example VSCode extensions use this to pick only settings that use this
* as a prefix in order to prevent conflicts and reduce the amount of data
* sent to the language server.
*
* @typedef {EngineFields & LanguageServerFields} Options
*
* @typedef UnifiedLanguageServerSettings
* @property {boolean} [requireConfig=false]
* If true, files will only be checked if a configuration file is present.
*/

import path from 'node:path'
Expand All @@ -42,6 +52,7 @@ import {
CodeActionKind,
Diagnostic,
DiagnosticSeverity,
DidChangeConfigurationNotification,
Position,
ProposedFeatures,
Range,
Expand Down Expand Up @@ -122,6 +133,7 @@ function lspDocumentToVfile(document, cwd) {
* Configuration for `unified-engine` and the language server.
*/
export function createUnifiedLanguageServer({
configurationSection,
ignoreName,
packageField,
pluginPrefix,
Expand All @@ -135,14 +147,49 @@ export function createUnifiedLanguageServer({
const documents = new TextDocuments(TextDocument)
/** @type {Set<string>} */
const workspaces = new Set()
/** @type {UnifiedLanguageServerSettings} */
const globalSettings = {requireConfig: false}
/** @type {Map<string, Promise<UnifiedLanguageServerSettings>>} */
const documentSettings = new Map()
let hasWorkspaceFolderCapability = false
let hasConfigurationCapability = false

/**
* @param {string} scopeUri
* @returns {Promise<UnifiedLanguageServerSettings>}
*/
async function getDocumentSettings(scopeUri) {
if (!hasConfigurationCapability) {
return globalSettings
}

let result = documentSettings.get(scopeUri)
if (!result) {
result = connection.workspace
.getConfiguration({scopeUri, section: configurationSection})
.then(
/** @param {Record<string, unknown>} raw */
(raw) => ({requireConfig: Boolean(raw.requireConfig)})
)
documentSettings.set(scopeUri, result)
}

return result
}

/**
* @param {string} cwd
* @param {VFile[]} files
* @param {boolean} alwaysStringify
* @param {boolean} ignoreUnconfigured
* @returns {Promise<VFile[]>}
*/
async function processWorkspace(cwd, files, alwaysStringify) {
async function processWorkspace(
cwd,
files,
alwaysStringify,
ignoreUnconfigured
) {
/** @type {EngineOptions['processor']} */
let processor

Expand Down Expand Up @@ -190,6 +237,7 @@ export function createUnifiedLanguageServer({
cwd,
files,
ignoreName,
ignoreUnconfigured,
packageField,
pluginPrefix,
plugins,
Expand Down Expand Up @@ -233,6 +281,8 @@ export function createUnifiedLanguageServer({
.sort((a, b) => b.length - a.length)
/** @type {Map<string, Array<VFile>>} */
const workspacePathToFiles = new Map()
/** @type {Map<string, Array<VFile>>} */
const workspacePathToFilesRequireConfig = new Map()

await Promise.all(
textDocuments.map(async (textDocument) => {
Expand Down Expand Up @@ -269,18 +319,28 @@ export function createUnifiedLanguageServer({

if (!cwd) return

const configuration = await getDocumentSettings(textDocument.uri)

const file = lspDocumentToVfile(textDocument, cwd)

const files = workspacePathToFiles.get(cwd) || []
workspacePathToFiles.set(cwd, [...files, file])
const filesMap = configuration.requireConfig
? workspacePathToFilesRequireConfig
: workspacePathToFiles
const files = filesMap.get(cwd) || []
files.push(file)
filesMap.set(cwd, files)
})
)

/** @type {Array<Promise<Array<VFile>>>} */
const promises = []

for (const [cwd, files] of workspacePathToFiles) {
promises.push(processWorkspace(cwd, files, alwaysStringify))
promises.push(processWorkspace(cwd, files, alwaysStringify, false))
}

for (const [cwd, files] of workspacePathToFilesRequireConfig) {
promises.push(processWorkspace(cwd, files, alwaysStringify, true))
}

const listsOfFiles = await Promise.all(promises)
Expand Down Expand Up @@ -324,6 +384,9 @@ export function createUnifiedLanguageServer({
workspaces.add(event.rootUri)
}

hasConfigurationCapability = Boolean(
event.capabilities.workspace && event.capabilities.workspace.configuration
)
hasWorkspaceFolderCapability = Boolean(
event.capabilities.workspace &&
event.capabilities.workspace.workspaceFolders
Expand All @@ -345,6 +408,10 @@ export function createUnifiedLanguageServer({
})

connection.onInitialized(() => {
if (hasConfigurationCapability) {
connection.client.register(DidChangeConfigurationNotification.type)
}

if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders((event) => {
for (const workspace of event.removed) {
Expand Down Expand Up @@ -399,13 +466,30 @@ export function createUnifiedLanguageServer({
version,
diagnostics: []
})
documentSettings.delete(uri)
})

// Check everything again if the file system watched by the client changes.
connection.onDidChangeWatchedFiles(() => {
checkDocuments(...documents.all())
})

connection.onDidChangeConfiguration((change) => {
if (hasConfigurationCapability) {
// Reset all cached document settings
documentSettings.clear()
} else {
globalSettings.requireConfig = Boolean(
/** @type {Omit<typeof change, 'settings'> & { settings: Record<string, unknown> }} */ (
change
).settings.requireConfig
)
}

// Revalidate all open text documents
checkDocuments(...documents.all())
})

connection.onCodeAction((event) => {
/** @type {CodeAction[]} */
const codeActions = []
Expand Down
6 changes: 6 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Create a **[language server][]** based on **[unified][]** ecosystems.
* [Examples](#examples)
* [Types](#types)
* [Language Server features](#language-server-features)
* [Configuration](#configuration)
* [Compatibility](#compatibility)
* [Related](#related)
* [Contribute](#contribute)
Expand Down Expand Up @@ -205,6 +206,11 @@ server features:
Any messages collected are published to the client using
`textDocument/publishDiagnostics`.

### Configuration

* `requireConfig` (default: `false`)
— If true, files will only be checked if a configuration file is present.

## Compatibility

Projects maintained by the unified collective are compatible with all maintained
Expand Down
1 change: 1 addition & 0 deletions test/code-actions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {createUnifiedLanguageServer} from 'unified-language-server'

createUnifiedLanguageServer({
configurationSection: 'remark',
processorName: 'remark',
processorSpecifier: 'remark',
plugins: [warn]
Expand Down
1 change: 1 addition & 0 deletions test/folder/remark-with-cwd.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {createUnifiedLanguageServer} from 'unified-language-server'

createUnifiedLanguageServer({
configurationSection: 'remark',
processorName: 'remark',
processorSpecifier: 'remark',
plugins: [warn]
Expand Down
Loading