From 535fa9ca827b8363eb8618cfb86a2ea99329ce8c Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Fri, 30 Aug 2024 19:58:40 +0200 Subject: [PATCH 1/6] Change custom request types to LSP commands LSP commands are the intended way to implement custom behaviour. --- .changeset/large-shoes-applaud.md | 6 ++ packages/language-server/README.md | 19 +++++ packages/language-server/index.js | 31 -------- .../test/syntax-toggle.test.js | 55 +++++++------- packages/language-service/README.md | 18 ++++- packages/language-service/index.js | 6 +- packages/language-service/lib/commands.js | 32 ++++---- .../language-service/lib/service-plugin.js | 37 ++++----- packages/vscode-mdx/package.json | 1 + packages/vscode-mdx/src/extension.js | 75 ++++++++++--------- tsconfig.base.json | 4 +- 11 files changed, 146 insertions(+), 138 deletions(-) create mode 100644 .changeset/large-shoes-applaud.md diff --git a/.changeset/large-shoes-applaud.md b/.changeset/large-shoes-applaud.md new file mode 100644 index 00000000..d3c39ffc --- /dev/null +++ b/.changeset/large-shoes-applaud.md @@ -0,0 +1,6 @@ +--- +'@mdx-js/language-service': minor +'@mdx-js/language-server': minor +--- + +Convert the custom MDX syntax toggle request types into LSP commands. diff --git a/packages/language-server/README.md b/packages/language-server/README.md index 0f5a225f..eb9cca67 100644 --- a/packages/language-server/README.md +++ b/packages/language-server/README.md @@ -65,6 +65,23 @@ This language server supports all features supported by [`volar-service-typescript`][volar-service-typescript], plus some additional features specific to MDX. +#### Commands + +The language server supports the following [LSP commands][]: + +* `mdx.toggleDelete` — Toggle delete syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. +* `mdx.toggleEmphasis` — Toggle emphasis syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. +* `mdx.toggleInlineCode` — Toggle inline code syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. +* `mdx.toggleStrong` — Toggle strong syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. + ### Initialize Options MDX language server supports the following LSP initialization options: @@ -271,6 +288,8 @@ Detailed changes for each release are documented in [CHANGELOG.md](./CHANGELOG.m [lsp]: https://microsoft.github.io/language-server-protocol +[lsp commands]: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#command + [mdx]: https://mdxjs.com [mit]: LICENSE diff --git a/packages/language-server/index.js b/packages/language-server/index.js index 8be9901b..eaf167a0 100755 --- a/packages/language-server/index.js +++ b/packages/language-server/index.js @@ -1,7 +1,6 @@ #!/usr/bin/env node /** - * @import {Commands} from '@mdx-js/language-service' * @import {PluggableList, Plugin} from 'unified' */ @@ -26,7 +25,6 @@ import remarkGfm from 'remark-gfm' import {create as createMarkdownServicePlugin} from 'volar-service-markdown' import {create as createTypeScriptServicePlugin} from 'volar-service-typescript' import {create as createTypeScriptSyntacticServicePlugin} from 'volar-service-typescript/lib/plugins/syntactic.js' -import {URI} from 'vscode-uri' process.title = 'mdx-language-server' @@ -123,26 +121,6 @@ connection.onInitialize(async (parameters) => { } }) -connection.onRequest('mdx/toggleDelete', async (parameters) => { - const commands = await getCommands(parameters.uri) - return commands.toggleDelete(parameters) -}) - -connection.onRequest('mdx/toggleEmphasis', async (parameters) => { - const commands = await getCommands(parameters.uri) - return commands.toggleEmphasis(parameters) -}) - -connection.onRequest('mdx/toggleInlineCode', async (parameters) => { - const commands = await getCommands(parameters.uri) - return commands.toggleInlineCode(parameters) -}) - -connection.onRequest('mdx/toggleStrong', async (parameters) => { - const commands = await getCommands(parameters.uri) - return commands.toggleStrong(parameters) -}) - connection.onInitialized(() => { const extensions = ['mdx'] if (tsEnabled) { @@ -164,12 +142,3 @@ connection.onInitialized(() => { }) connection.listen() - -/** - * @param {string} uri - * @returns {Promise} - */ -async function getCommands(uri) { - const service = await server.project.getLanguageService(URI.parse(uri)) - return service.context.inject('mdxCommands') -} diff --git a/packages/language-server/test/syntax-toggle.test.js b/packages/language-server/test/syntax-toggle.test.js index 7c13541e..7d661437 100644 --- a/packages/language-server/test/syntax-toggle.test.js +++ b/packages/language-server/test/syntax-toggle.test.js @@ -1,7 +1,8 @@ /** * @import {LanguageServerHandle} from '@volar/test-utils' - * @import {SyntaxToggleParams} from '@mdx-js/language-service' + * @import {Range} from '@volar/language-server' */ + import assert from 'node:assert/strict' import {afterEach, beforeEach, test} from 'node:test' import {createServer, fixtureUri, tsdk} from './utils.js' @@ -22,12 +23,13 @@ afterEach(() => { test('delete', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') - const result = await serverHandle.connection.sendRequest( - 'mdx/toggleDelete', - /** @satisfies {SyntaxToggleParams} */ ({ - uri: 'memory://1', - range: {end: {character: 3, line: 0}, start: {character: 3, line: 0}} - }) + const result = await serverHandle.sendExecuteCommandRequest( + 'mdx.toggleDelete', + [ + 'memory://1', + /** @satisfies {Range} */ + ({end: {character: 3, line: 0}, start: {character: 3, line: 0}}) + ] ) assert.deepEqual(result, [ @@ -44,12 +46,13 @@ test('delete', async () => { test('emphasis', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') - const result = await serverHandle.connection.sendRequest( - 'mdx/toggleEmphasis', - /** @satisfies {SyntaxToggleParams} */ ({ - uri: 'memory://1', - range: {end: {character: 3, line: 0}, start: {character: 3, line: 0}} - }) + const result = await serverHandle.sendExecuteCommandRequest( + 'mdx.toggleEmphasis', + [ + 'memory://1', + /** @satisfies {Range} */ + ({end: {character: 3, line: 0}, start: {character: 3, line: 0}}) + ] ) assert.deepEqual(result, [ @@ -66,12 +69,13 @@ test('emphasis', async () => { test('inlineCode', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') - const result = await serverHandle.connection.sendRequest( - 'mdx/toggleInlineCode', - /** @satisfies {SyntaxToggleParams} */ ({ - uri: 'memory://1', - range: {end: {character: 3, line: 0}, start: {character: 3, line: 0}} - }) + const result = await serverHandle.sendExecuteCommandRequest( + 'mdx.toggleInlineCode', + [ + 'memory://1', + /** @satisfies {Range} */ + ({end: {character: 3, line: 0}, start: {character: 3, line: 0}}) + ] ) assert.deepEqual(result, [ @@ -88,12 +92,13 @@ test('inlineCode', async () => { test('strong', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') - const result = await serverHandle.connection.sendRequest( - 'mdx/toggleStrong', - /** @satisfies {SyntaxToggleParams} */ ({ - uri: 'memory://1', - range: {end: {character: 3, line: 0}, start: {character: 3, line: 0}} - }) + const result = await serverHandle.sendExecuteCommandRequest( + 'mdx.toggleStrong', + [ + 'memory://1', + /** @satisfies {Range} */ + ({end: {character: 3, line: 0}, start: {character: 3, line: 0}}) + ] ) assert.deepEqual(result, [ diff --git a/packages/language-service/README.md b/packages/language-service/README.md index be3b4bad..4b55dcef 100644 --- a/packages/language-service/README.md +++ b/packages/language-service/README.md @@ -110,8 +110,22 @@ The service supports: * Reporting diagnostics for parsing errors. * Document drop support for images. -* Custom commands for toggling `delete`, `emphasis`, `inlineCode`, and `strong` - text. +* Custom commands. + +The following commands are supported: + +* `mdx.toggleDelete` — Toggle delete syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. +* `mdx.toggleEmphasis` — Toggle emphasis syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. +* `mdx.toggleInlineCode` — Toggle inline code syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. +* `mdx.toggleStrong` — Toggle strong syntax at the cursor position. + This takes the URI as its first argument, and the LSP selection range as its + second argument. #### Parameters diff --git a/packages/language-service/index.js b/packages/language-service/index.js index 8f6a8cd7..fe1d18e6 100644 --- a/packages/language-service/index.js +++ b/packages/language-service/index.js @@ -1,8 +1,4 @@ -/** - * @typedef {import('./lib/commands.js').SyntaxToggleParams} SyntaxToggleParams - * @typedef {import('./lib/service-plugin.js').Commands} Commands - */ - +export {commands} from './lib/commands.js' export {createMdxLanguagePlugin} from './lib/language-plugin.js' export {createMdxServicePlugin} from './lib/service-plugin.js' export {resolveRemarkPlugins} from './lib/tsconfig.js' diff --git a/packages/language-service/lib/commands.js b/packages/language-service/lib/commands.js index 88c3d01e..61169bae 100644 --- a/packages/language-service/lib/commands.js +++ b/packages/language-service/lib/commands.js @@ -3,20 +3,15 @@ * @import {Nodes} from 'mdast' */ -/** - * @typedef SyntaxToggleParams - * The request parameters for LSP toggle requests. - * @property {string} uri - * The URI of the document the request is for. - * @property {Range} range - * The range that is selected by the user. - */ - /** * @callback SyntaxToggle * A function to toggle prose markdown syntax based on the AST. - * @param {SyntaxToggleParams} params - * The input parameters from the LSP request. + * @param {LanguageServiceContext} context + * The Volar service context. + * @param {string} uri + * The URI of the document the request is for. + * @param {Range} range + * The range that is selected by the user. * @returns {TextEdit[] | undefined} * LSP text edits that should be made. */ @@ -29,8 +24,6 @@ import {VirtualMdxCode} from './virtual-code.js' /** * Create a function to toggle prose syntax based on the AST. * - * @param {LanguageServiceContext} context - * The Volar service context. * @param {Nodes['type']} type * The type of the mdast node to toggle. * @param {string} separator @@ -38,8 +31,8 @@ import {VirtualMdxCode} from './virtual-code.js' * @returns {SyntaxToggle} * An LSP based syntax toggle function. */ -export function createSyntaxToggle(context, type, separator) { - return ({range, uri}) => { +function createSyntaxToggle(type, separator) { + return (context, uri, range) => { const parsedUri = URI.parse(uri) const sourceScript = context.language.scripts.get(parsedUri) const root = sourceScript?.generated?.root @@ -146,3 +139,12 @@ export function createSyntaxToggle(context, type, separator) { } } } + +export const implementations = { + 'mdx.toggleDelete': createSyntaxToggle('delete', '~'), + 'mdx.toggleEmphasis': createSyntaxToggle('emphasis', '_'), + 'mdx.toggleInlineCode': createSyntaxToggle('inlineCode', '`'), + 'mdx.toggleStrong': createSyntaxToggle('strong', '**') +} + +export const commands = Object.keys(implementations) diff --git a/packages/language-service/lib/service-plugin.js b/packages/language-service/lib/service-plugin.js index a00bd451..deda32de 100644 --- a/packages/language-service/lib/service-plugin.js +++ b/packages/language-service/lib/service-plugin.js @@ -1,26 +1,12 @@ /** - * @import {DataTransferItem, LanguageServicePlugin} from '@volar/language-service' - * @import {SyntaxToggle} from './commands.js' - */ - -/** - * @typedef Commands - * @property {SyntaxToggle} toggleDelete - * @property {SyntaxToggle} toggleEmphasis - * @property {SyntaxToggle} toggleInlineCode - * @property {SyntaxToggle} toggleStrong - */ - -/** - * @typedef Provide - * @property {() => Commands} mdxCommands + * @import {DataTransferItem, LanguageServicePlugin, Range} from '@volar/language-service' */ import path from 'node:path/posix' import {toMarkdown} from 'mdast-util-to-markdown' import {fromPlace} from 'unist-util-lsp' import {URI, Utils} from 'vscode-uri' -import {createSyntaxToggle} from './commands.js' +import {commands, implementations} from './commands.js' import {VirtualMdxCode} from './virtual-code.js' // https://github.com/microsoft/vscode/blob/1.83.1/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts#L29-L41 @@ -62,19 +48,22 @@ export function createMdxServicePlugin() { interFileDependencies: false, workspaceDiagnostics: false }, + executeCommandProvider: { + commands + }, documentDropEditsProvider: true }, create(context) { return { - provide: { - mdxCommands() { - return { - toggleDelete: createSyntaxToggle(context, 'delete', '~'), - toggleEmphasis: createSyntaxToggle(context, 'emphasis', '_'), - toggleInlineCode: createSyntaxToggle(context, 'inlineCode', '`'), - toggleStrong: createSyntaxToggle(context, 'strong', '**') - } + executeCommand(command, args) { + if ( + command in implementations && + Object.hasOwn(implementations, command) + ) { + const fn = + implementations[/** @type {keyof implementations} */ (command)] + return fn(context, .../** @type {[string, Range]} */ (args)) } }, diff --git a/packages/vscode-mdx/package.json b/packages/vscode-mdx/package.json index 2fda5d66..f56e1ce1 100644 --- a/packages/vscode-mdx/package.json +++ b/packages/vscode-mdx/package.json @@ -39,6 +39,7 @@ "vscode:prepublish": "node ./script/build.mjs" }, "devDependencies": { + "@mdx-js/language-service": "^0.5.0", "@types/node": "^22.0.0", "@types/vscode": "^1.82.0", "@volar/language-server": "~2.4.0", diff --git a/packages/vscode-mdx/src/extension.js b/packages/vscode-mdx/src/extension.js index 8d056e40..1c6e1659 100644 --- a/packages/vscode-mdx/src/extension.js +++ b/packages/vscode-mdx/src/extension.js @@ -1,8 +1,9 @@ /** - * @import {LabsInfo, TextEdit} from '@volar/vscode' + * @import {ExecuteCommandSignature, LabsInfo, TextEdit} from '@volar/vscode' * @import {ExtensionContext} from 'vscode' */ +import {commands} from '@mdx-js/language-service' import * as languageServerProtocol from '@volar/language-server/protocol.js' import { activateAutoInsertion, @@ -11,7 +12,6 @@ import { getTsdk } from '@volar/vscode' import { - commands, extensions, window, workspace, @@ -60,7 +60,8 @@ export async function activate(context) { markdown: { isTrusted: true, supportHtml: true - } + }, + middleware: {executeCommand} } ) @@ -117,11 +118,7 @@ async function startServer() { disposable = Disposable.from( activateAutoInsertion('mdx', client), - activateDocumentDropEdit('mdx', client), - activateMdxToggleCommand('toggleDelete'), - activateMdxToggleCommand('toggleEmphasis'), - activateMdxToggleCommand('toggleInlineCode'), - activateMdxToggleCommand('toggleStrong') + activateDocumentDropEdit('mdx', client) ) } ) @@ -129,37 +126,47 @@ async function startServer() { } /** - * @param {string} command - * @returns {Disposable} + * Execute a command with correct arguments. + * + * @param {string} name + * The name of the command to execute. + * @param {unknown[]} args + * The original arguments passed to the command. + * @param {ExecuteCommandSignature} next + * The next middleware to execute. + * @returns {Promise} + * The command result. */ -function activateMdxToggleCommand(command) { - return commands.registerCommand('mdx.' + command, async () => { - const editor = window.activeTextEditor - if (!editor) { - return - } +async function executeCommand(name, args, next) { + if (!commands.includes(name)) { + return next(name, args) + } + + const editor = window.activeTextEditor + if (!editor) { + return + } - const document = editor.document - const beforeVersion = document.version + const document = editor.document + const beforeVersion = document.version - /** @type {TextEdit[] | undefined} */ - const response = await client.sendRequest('mdx/' + command, { - uri: String(document.uri), - range: client.code2ProtocolConverter.asRange(editor.selection) - }) + /** @type {TextEdit[] | undefined} */ + const response = await next(name, [ + String(document.uri), + client.code2ProtocolConverter.asRange(editor.selection) + ]) - if (!response?.length) { - return - } + if (!response?.length) { + return + } - const textEdits = await client.protocol2CodeConverter.asTextEdits(response) + const textEdits = await client.protocol2CodeConverter.asTextEdits(response) - if (beforeVersion !== document.version) { - return - } + if (beforeVersion !== document.version) { + return + } - const workspaceEdit = new WorkspaceEdit() - workspaceEdit.set(document.uri, textEdits) - workspace.applyEdit(workspaceEdit, {}) - }) + const workspaceEdit = new WorkspaceEdit() + workspaceEdit.set(document.uri, textEdits) + workspace.applyEdit(workspaceEdit, {}) } diff --git a/tsconfig.base.json b/tsconfig.base.json index f073dc16..519c1a35 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -4,11 +4,11 @@ "checkJs": true, "composite": true, "declarationMap": true, - "lib": ["es2021"], + "lib": ["es2022"], "module": "node16", "moduleDetection": "force", "strict": true, "stripInternal": true, - "target": "es2021" + "target": "es2022" } } From 7e982e64f068400b8a3706a6a0cabe770e63afde Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sat, 31 Aug 2024 13:21:51 +0200 Subject: [PATCH 2/6] Fix a broken test --- packages/language-server/test/initialize.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/language-server/test/initialize.test.js b/packages/language-server/test/initialize.test.js index a2c2da5c..61d112f3 100644 --- a/packages/language-server/test/initialize.test.js +++ b/packages/language-server/test/initialize.test.js @@ -55,6 +55,14 @@ test('initialize', async () => { }, documentRangeFormattingProvider: true, documentSymbolProvider: true, + executeCommandProvider: { + commands: [ + 'mdx.toggleDelete', + 'mdx.toggleEmphasis', + 'mdx.toggleInlineCode', + 'mdx.toggleStrong' + ] + }, experimental: { autoInsertionProvider: { configurationSections: [ From 07cd18f9b201553016b7bc529cbbf9be0455801e Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sun, 8 Sep 2024 15:10:25 +0200 Subject: [PATCH 3/6] Refactor commands Commands now use a switch to differentiate between commands. Existing logic was too clever. The currently existing commands are similar, but future commands might not be. The new pattern is more flexible. --- packages/language-service/index.js | 1 - packages/language-service/lib/commands.js | 204 ++++++++---------- .../language-service/lib/service-plugin.js | 36 +++- packages/vscode-mdx/package.json | 1 - packages/vscode-mdx/src/extension.js | 62 +++--- 5 files changed, 155 insertions(+), 149 deletions(-) diff --git a/packages/language-service/index.js b/packages/language-service/index.js index fe1d18e6..a55cb588 100644 --- a/packages/language-service/index.js +++ b/packages/language-service/index.js @@ -1,4 +1,3 @@ -export {commands} from './lib/commands.js' export {createMdxLanguagePlugin} from './lib/language-plugin.js' export {createMdxServicePlugin} from './lib/service-plugin.js' export {resolveRemarkPlugins} from './lib/tsconfig.js' diff --git a/packages/language-service/lib/commands.js b/packages/language-service/lib/commands.js index 61169bae..95203d9e 100644 --- a/packages/language-service/lib/commands.js +++ b/packages/language-service/lib/commands.js @@ -3,148 +3,130 @@ * @import {Nodes} from 'mdast' */ -/** - * @callback SyntaxToggle - * A function to toggle prose markdown syntax based on the AST. - * @param {LanguageServiceContext} context - * The Volar service context. - * @param {string} uri - * The URI of the document the request is for. - * @param {Range} range - * The range that is selected by the user. - * @returns {TextEdit[] | undefined} - * LSP text edits that should be made. - */ - import {visitParents} from 'unist-util-visit-parents' import {URI} from 'vscode-uri' import {getNodeEndOffset, getNodeStartOffset} from './mdast-utils.js' import {VirtualMdxCode} from './virtual-code.js' /** - * Create a function to toggle prose syntax based on the AST. + * Toggle prose syntax based on the AST. * + * @param {LanguageServiceContext} context + * The Volar service context. * @param {Nodes['type']} type * The type of the mdast node to toggle. * @param {string} separator * The mdast node separator to insert. - * @returns {SyntaxToggle} - * An LSP based syntax toggle function. + * @param {string} uri + * The URI of the document the request is for. + * @param {Range} range + * The range that is selected by the user. + * @returns {TextEdit[] | undefined} + * LSP text edits that should be made. */ -function createSyntaxToggle(type, separator) { - return (context, uri, range) => { - const parsedUri = URI.parse(uri) - const sourceScript = context.language.scripts.get(parsedUri) - const root = sourceScript?.generated?.root +export function toggleSyntax(context, type, separator, uri, range) { + const parsedUri = URI.parse(uri) + const sourceScript = context.language.scripts.get(parsedUri) + const root = sourceScript?.generated?.root - if (!(root instanceof VirtualMdxCode)) { - return - } + if (!(root instanceof VirtualMdxCode)) { + return + } - const ast = root.ast + const ast = root.ast - if (!ast) { - return - } + if (!ast) { + return + } - const doc = context.documents.get(parsedUri, root.languageId, root.snapshot) - const selectionStart = doc.offsetAt(range.start) - const selectionEnd = doc.offsetAt(range.end) + const doc = context.documents.get(parsedUri, root.languageId, root.snapshot) + const selectionStart = doc.offsetAt(range.start) + const selectionEnd = doc.offsetAt(range.end) - /** @type {TextEdit[]} */ - const edits = [] + /** @type {TextEdit[]} */ + const edits = [] - visitParents(ast, 'text', (node, ancestors) => { - const nodeStart = getNodeStartOffset(node) - const nodeEnd = getNodeEndOffset(node) + visitParents(ast, 'text', (node, ancestors) => { + const nodeStart = getNodeStartOffset(node) + const nodeEnd = getNodeEndOffset(node) - if (selectionStart < nodeStart) { - // Outside of this node - return - } + if (selectionStart < nodeStart) { + // Outside of this node + return + } - if (selectionEnd > nodeEnd) { - // Outside of this node - return - } + if (selectionEnd > nodeEnd) { + // Outside of this node + return + } + + const matchingAncestor = ancestors.find( + (ancestor) => ancestor.type === type + ) - const matchingAncestor = ancestors.find( - (ancestor) => ancestor.type === type + if (matchingAncestor) { + const ancestorStart = getNodeStartOffset(matchingAncestor) + const ancestorEnd = getNodeEndOffset(matchingAncestor) + const firstChildStart = getNodeStartOffset(matchingAncestor.children[0]) + const lastChildEnd = getNodeEndOffset( + /** @type {Nodes} */ (matchingAncestor.children.at(-1)) ) - if (matchingAncestor) { - const ancestorStart = getNodeStartOffset(matchingAncestor) - const ancestorEnd = getNodeEndOffset(matchingAncestor) - const firstChildStart = getNodeStartOffset(matchingAncestor.children[0]) - const lastChildEnd = getNodeEndOffset( - /** @type {Nodes} */ (matchingAncestor.children.at(-1)) - ) - - edits.push( - { - newText: '', - range: { - start: doc.positionAt(ancestorStart), - end: doc.positionAt(firstChildStart) - } - }, - { - newText: '', - range: { - start: doc.positionAt(lastChildEnd), - end: doc.positionAt(ancestorEnd) - } + edits.push( + { + newText: '', + range: { + start: doc.positionAt(ancestorStart), + end: doc.positionAt(firstChildStart) } - ) - } else { - const valueOffset = getNodeStartOffset(node) - let insertStart = valueOffset - let insertEnd = getNodeEndOffset(node) - - for (const match of node.value.matchAll(/\b/g)) { - if (match.index === undefined) { - continue + }, + { + newText: '', + range: { + start: doc.positionAt(lastChildEnd), + end: doc.positionAt(ancestorEnd) } + } + ) + } else { + const valueOffset = getNodeStartOffset(node) + let insertStart = valueOffset + let insertEnd = getNodeEndOffset(node) + + for (const match of node.value.matchAll(/\b/g)) { + if (match.index === undefined) { + continue + } - const matchOffset = valueOffset + match.index - - if (matchOffset <= selectionStart) { - insertStart = matchOffset - continue - } + const matchOffset = valueOffset + match.index - if (matchOffset >= selectionEnd) { - insertEnd = matchOffset - break - } + if (matchOffset <= selectionStart) { + insertStart = matchOffset + continue } - const startPosition = doc.positionAt(insertStart) - const endPosition = doc.positionAt(insertEnd) - edits.push( - { - newText: separator, - range: {start: startPosition, end: startPosition} - }, - { - newText: separator, - range: {start: endPosition, end: endPosition} - } - ) + if (matchOffset >= selectionEnd) { + insertEnd = matchOffset + break + } } - }) - if (edits) { - return edits + const startPosition = doc.positionAt(insertStart) + const endPosition = doc.positionAt(insertEnd) + edits.push( + { + newText: separator, + range: {start: startPosition, end: startPosition} + }, + { + newText: separator, + range: {start: endPosition, end: endPosition} + } + ) } - } -} + }) -export const implementations = { - 'mdx.toggleDelete': createSyntaxToggle('delete', '~'), - 'mdx.toggleEmphasis': createSyntaxToggle('emphasis', '_'), - 'mdx.toggleInlineCode': createSyntaxToggle('inlineCode', '`'), - 'mdx.toggleStrong': createSyntaxToggle('strong', '**') + if (edits) { + return edits + } } - -export const commands = Object.keys(implementations) diff --git a/packages/language-service/lib/service-plugin.js b/packages/language-service/lib/service-plugin.js index deda32de..f6765975 100644 --- a/packages/language-service/lib/service-plugin.js +++ b/packages/language-service/lib/service-plugin.js @@ -6,7 +6,7 @@ import path from 'node:path/posix' import {toMarkdown} from 'mdast-util-to-markdown' import {fromPlace} from 'unist-util-lsp' import {URI, Utils} from 'vscode-uri' -import {commands, implementations} from './commands.js' +import {toggleSyntax} from './commands.js' import {VirtualMdxCode} from './virtual-code.js' // https://github.com/microsoft/vscode/blob/1.83.1/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts#L29-L41 @@ -49,7 +49,12 @@ export function createMdxServicePlugin() { workspaceDiagnostics: false }, executeCommandProvider: { - commands + commands: [ + 'mdx.toggleDelete', + 'mdx.toggleEmphasis', + 'mdx.toggleInlineCode', + 'mdx.toggleStrong' + ] }, documentDropEditsProvider: true }, @@ -57,13 +62,26 @@ export function createMdxServicePlugin() { create(context) { return { executeCommand(command, args) { - if ( - command in implementations && - Object.hasOwn(implementations, command) - ) { - const fn = - implementations[/** @type {keyof implementations} */ (command)] - return fn(context, .../** @type {[string, Range]} */ (args)) + switch (command) { + case 'mdx.toggleDelete': { + return toggleSyntax(context, 'delete', '~', args[0], args[1]) + } + + case 'mdx.toggleEmphasis': { + return toggleSyntax(context, 'emphasis', '_', args[0], args[1]) + } + + case 'mdx.toggleInlineCode': { + return toggleSyntax(context, 'inlineCode', '`', args[0], args[1]) + } + + case 'mdx.toggleStrong': { + return toggleSyntax(context, 'strong', '**', args[0], args[1]) + } + + default: { + throw new Error('Unknown command: ' + command) + } } }, diff --git a/packages/vscode-mdx/package.json b/packages/vscode-mdx/package.json index f56e1ce1..2fda5d66 100644 --- a/packages/vscode-mdx/package.json +++ b/packages/vscode-mdx/package.json @@ -39,7 +39,6 @@ "vscode:prepublish": "node ./script/build.mjs" }, "devDependencies": { - "@mdx-js/language-service": "^0.5.0", "@types/node": "^22.0.0", "@types/vscode": "^1.82.0", "@volar/language-server": "~2.4.0", diff --git a/packages/vscode-mdx/src/extension.js b/packages/vscode-mdx/src/extension.js index 1c6e1659..696a85d6 100644 --- a/packages/vscode-mdx/src/extension.js +++ b/packages/vscode-mdx/src/extension.js @@ -3,7 +3,6 @@ * @import {ExtensionContext} from 'vscode' */ -import {commands} from '@mdx-js/language-service' import * as languageServerProtocol from '@volar/language-server/protocol.js' import { activateAutoInsertion, @@ -128,7 +127,7 @@ async function startServer() { /** * Execute a command with correct arguments. * - * @param {string} name + * @param {string} command * The name of the command to execute. * @param {unknown[]} args * The original arguments passed to the command. @@ -137,36 +136,45 @@ async function startServer() { * @returns {Promise} * The command result. */ -async function executeCommand(name, args, next) { - if (!commands.includes(name)) { - return next(name, args) - } +async function executeCommand(command, args, next) { + switch (command) { + case 'mdx.toggleDelete': + case 'mdx.toggleEmphasis': + case 'mdx.toggleInlineCode': + case 'mdx.toggleStrong': { + const editor = window.activeTextEditor + if (!editor) { + return + } - const editor = window.activeTextEditor - if (!editor) { - return - } + const document = editor.document + const beforeVersion = document.version - const document = editor.document - const beforeVersion = document.version + /** @type {TextEdit[] | undefined} */ + const response = await next(command, [ + String(document.uri), + client.code2ProtocolConverter.asRange(editor.selection) + ]) - /** @type {TextEdit[] | undefined} */ - const response = await next(name, [ - String(document.uri), - client.code2ProtocolConverter.asRange(editor.selection) - ]) + if (!response?.length) { + return + } - if (!response?.length) { - return - } + const textEdits = + await client.protocol2CodeConverter.asTextEdits(response) - const textEdits = await client.protocol2CodeConverter.asTextEdits(response) + if (beforeVersion !== document.version) { + return + } - if (beforeVersion !== document.version) { - return - } + const workspaceEdit = new WorkspaceEdit() + workspaceEdit.set(document.uri, textEdits) + workspace.applyEdit(workspaceEdit, {}) + return + } - const workspaceEdit = new WorkspaceEdit() - workspaceEdit.set(document.uri, textEdits) - workspace.applyEdit(workspaceEdit, {}) + default: { + return next(command, args) + } + } } From d1e4e8102cdb2500d5b7589d0ec0d1263da51cfc Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sun, 8 Sep 2024 15:57:54 +0200 Subject: [PATCH 4/6] Use workspace/applyEdit commands to apply text changes This means clients need to implement less custom logic. --- packages/language-server/index.js | 2 +- .../test/syntax-toggle.test.js | 136 +++++++++++++----- packages/language-service/lib/commands.js | 19 ++- .../language-service/lib/service-plugin.js | 55 ++++++- packages/vscode-mdx/src/extension.js | 27 +--- 5 files changed, 167 insertions(+), 72 deletions(-) diff --git a/packages/language-server/index.js b/packages/language-server/index.js index eaf167a0..9e695e56 100755 --- a/packages/language-server/index.js +++ b/packages/language-server/index.js @@ -66,7 +66,7 @@ connection.onInitialize(async (parameters) => { return context.env.getConfiguration?.('mdx.validate') } }), - createMdxServicePlugin() + createMdxServicePlugin(connection.workspace) ] if (tsEnabled) { diff --git a/packages/language-server/test/syntax-toggle.test.js b/packages/language-server/test/syntax-toggle.test.js index 7d661437..f048c70f 100644 --- a/packages/language-server/test/syntax-toggle.test.js +++ b/packages/language-server/test/syntax-toggle.test.js @@ -23,6 +23,9 @@ afterEach(() => { test('delete', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') + const editsPromise = new Promise((resolve) => { + serverHandle.connection.onRequest('workspace/applyEdit', resolve) + }) const result = await serverHandle.sendExecuteCommandRequest( 'mdx.toggleDelete', [ @@ -32,20 +35,36 @@ test('delete', async () => { ] ) - assert.deepEqual(result, [ - { - newText: '~', - range: {end: {character: 0, line: 0}, start: {character: 0, line: 0}} - }, - { - newText: '~', - range: {end: {character: 5, line: 0}, start: {character: 5, line: 0}} + assert.equal(result, null) + assert.deepEqual(await editsPromise, { + edit: { + changes: { + 'memory://1': [ + { + newText: '~', + range: { + end: {character: 0, line: 0}, + start: {character: 0, line: 0} + } + }, + { + newText: '~', + range: { + end: {character: 5, line: 0}, + start: {character: 5, line: 0} + } + } + ] + } } - ]) + }) }) test('emphasis', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') + const editsPromise = new Promise((resolve) => { + serverHandle.connection.onRequest('workspace/applyEdit', resolve) + }) const result = await serverHandle.sendExecuteCommandRequest( 'mdx.toggleEmphasis', [ @@ -55,20 +74,36 @@ test('emphasis', async () => { ] ) - assert.deepEqual(result, [ - { - newText: '_', - range: {end: {character: 0, line: 0}, start: {character: 0, line: 0}} - }, - { - newText: '_', - range: {end: {character: 5, line: 0}, start: {character: 5, line: 0}} + assert.equal(result, null) + assert.deepEqual(await editsPromise, { + edit: { + changes: { + 'memory://1': [ + { + newText: '_', + range: { + end: {character: 0, line: 0}, + start: {character: 0, line: 0} + } + }, + { + newText: '_', + range: { + end: {character: 5, line: 0}, + start: {character: 5, line: 0} + } + } + ] + } } - ]) + }) }) test('inlineCode', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') + const editsPromise = new Promise((resolve) => { + serverHandle.connection.onRequest('workspace/applyEdit', resolve) + }) const result = await serverHandle.sendExecuteCommandRequest( 'mdx.toggleInlineCode', [ @@ -78,20 +113,36 @@ test('inlineCode', async () => { ] ) - assert.deepEqual(result, [ - { - newText: '`', - range: {end: {character: 0, line: 0}, start: {character: 0, line: 0}} - }, - { - newText: '`', - range: {end: {character: 5, line: 0}, start: {character: 5, line: 0}} + assert.equal(result, null) + assert.deepEqual(await editsPromise, { + edit: { + changes: { + 'memory://1': [ + { + newText: '`', + range: { + end: {character: 0, line: 0}, + start: {character: 0, line: 0} + } + }, + { + newText: '`', + range: { + end: {character: 5, line: 0}, + start: {character: 5, line: 0} + } + } + ] + } } - ]) + }) }) test('strong', async () => { await serverHandle.openInMemoryDocument('memory://1', 'mdx', 'Hello\n') + const editsPromise = new Promise((resolve) => { + serverHandle.connection.onRequest('workspace/applyEdit', resolve) + }) const result = await serverHandle.sendExecuteCommandRequest( 'mdx.toggleStrong', [ @@ -101,14 +152,27 @@ test('strong', async () => { ] ) - assert.deepEqual(result, [ - { - newText: '**', - range: {end: {character: 0, line: 0}, start: {character: 0, line: 0}} - }, - { - newText: '**', - range: {end: {character: 5, line: 0}, start: {character: 5, line: 0}} + assert.equal(result, null) + assert.deepEqual(await editsPromise, { + edit: { + changes: { + 'memory://1': [ + { + newText: '**', + range: { + end: {character: 0, line: 0}, + start: {character: 0, line: 0} + } + }, + { + newText: '**', + range: { + end: {character: 5, line: 0}, + start: {character: 5, line: 0} + } + } + ] + } } - ]) + }) }) diff --git a/packages/language-service/lib/commands.js b/packages/language-service/lib/commands.js index 95203d9e..0a33bb9e 100644 --- a/packages/language-service/lib/commands.js +++ b/packages/language-service/lib/commands.js @@ -1,6 +1,7 @@ /** * @import {LanguageServiceContext, Range, TextEdit} from '@volar/language-service' * @import {Nodes} from 'mdast' + * @import {createMdxServicePlugin} from './service-plugin.js' */ import {visitParents} from 'unist-util-visit-parents' @@ -13,6 +14,8 @@ import {VirtualMdxCode} from './virtual-code.js' * * @param {LanguageServiceContext} context * The Volar service context. + * @param {createMdxServicePlugin.Options} options + * The options to use for applying workspace edits. * @param {Nodes['type']} type * The type of the mdast node to toggle. * @param {string} separator @@ -21,10 +24,16 @@ import {VirtualMdxCode} from './virtual-code.js' * The URI of the document the request is for. * @param {Range} range * The range that is selected by the user. - * @returns {TextEdit[] | undefined} - * LSP text edits that should be made. + * @returns {Promise} */ -export function toggleSyntax(context, type, separator, uri, range) { +export async function toggleSyntax( + context, + options, + type, + separator, + uri, + range +) { const parsedUri = URI.parse(uri) const sourceScript = context.language.scripts.get(parsedUri) const root = sourceScript?.generated?.root @@ -126,7 +135,7 @@ export function toggleSyntax(context, type, separator, uri, range) { } }) - if (edits) { - return edits + if (edits.length > 0) { + await options.applyEdit({changes: {[uri]: edits}}) } } diff --git a/packages/language-service/lib/service-plugin.js b/packages/language-service/lib/service-plugin.js index f6765975..da0171b3 100644 --- a/packages/language-service/lib/service-plugin.js +++ b/packages/language-service/lib/service-plugin.js @@ -1,5 +1,5 @@ /** - * @import {DataTransferItem, LanguageServicePlugin, Range} from '@volar/language-service' + * @import {DataTransferItem, LanguageServicePlugin, WorkspaceEdit} from '@volar/language-service' */ import path from 'node:path/posix' @@ -9,6 +9,19 @@ import {URI, Utils} from 'vscode-uri' import {toggleSyntax} from './commands.js' import {VirtualMdxCode} from './virtual-code.js' +/** + * @callback ApplyEdit + * @param {WorkspaceEdit} edit + * The workspace edit to apply. + * @returns {PromiseLike} + */ + +/** + * @typedef createMdxServicePlugin.Options + * @property {ApplyEdit} applyEdit + * A function to apply workspace edits. + */ + // https://github.com/microsoft/vscode/blob/1.83.1/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts#L29-L41 const imageExtensions = new Set([ '.bmp', @@ -36,10 +49,12 @@ const imageExtensions = new Set([ * - Custom commands for toggling `delete`, `emphasis`, `inlineCode`, and * `strong` text. * + * @param {createMdxServicePlugin.Options} options + * Options to configure the MDX language service. * @returns {LanguageServicePlugin} * The Volar service plugin for MDX files. */ -export function createMdxServicePlugin() { +export function createMdxServicePlugin(options) { return { name: 'mdx', @@ -64,19 +79,47 @@ export function createMdxServicePlugin() { executeCommand(command, args) { switch (command) { case 'mdx.toggleDelete': { - return toggleSyntax(context, 'delete', '~', args[0], args[1]) + return toggleSyntax( + context, + options, + 'delete', + '~', + args[0], + args[1] + ) } case 'mdx.toggleEmphasis': { - return toggleSyntax(context, 'emphasis', '_', args[0], args[1]) + return toggleSyntax( + context, + options, + 'emphasis', + '_', + args[0], + args[1] + ) } case 'mdx.toggleInlineCode': { - return toggleSyntax(context, 'inlineCode', '`', args[0], args[1]) + return toggleSyntax( + context, + options, + 'inlineCode', + '`', + args[0], + args[1] + ) } case 'mdx.toggleStrong': { - return toggleSyntax(context, 'strong', '**', args[0], args[1]) + return toggleSyntax( + context, + options, + 'strong', + '**', + args[0], + args[1] + ) } default: { diff --git a/packages/vscode-mdx/src/extension.js b/packages/vscode-mdx/src/extension.js index 696a85d6..c06f7037 100644 --- a/packages/vscode-mdx/src/extension.js +++ b/packages/vscode-mdx/src/extension.js @@ -15,8 +15,7 @@ import { window, workspace, Disposable, - ProgressLocation, - WorkspaceEdit + ProgressLocation } from 'vscode' import {LanguageClient, TransportKind} from '@volar/vscode/node.js' @@ -147,30 +146,10 @@ async function executeCommand(command, args, next) { return } - const document = editor.document - const beforeVersion = document.version - - /** @type {TextEdit[] | undefined} */ - const response = await next(command, [ - String(document.uri), + return next(command, [ + String(editor.document.uri), client.code2ProtocolConverter.asRange(editor.selection) ]) - - if (!response?.length) { - return - } - - const textEdits = - await client.protocol2CodeConverter.asTextEdits(response) - - if (beforeVersion !== document.version) { - return - } - - const workspaceEdit = new WorkspaceEdit() - workspaceEdit.set(document.uri, textEdits) - workspace.applyEdit(workspaceEdit, {}) - return } default: { From cf1da61765ac5b929af9e81474119fc67e7b58ed Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sun, 8 Sep 2024 16:12:55 +0200 Subject: [PATCH 5/6] Update documentation --- packages/language-server/README.md | 67 +++++++++++++++++++++++------ packages/language-service/README.md | 7 +-- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/language-server/README.md b/packages/language-server/README.md index eb9cca67..69370b72 100644 --- a/packages/language-server/README.md +++ b/packages/language-server/README.md @@ -69,18 +69,61 @@ features specific to MDX. The language server supports the following [LSP commands][]: -* `mdx.toggleDelete` — Toggle delete syntax at the cursor position. - This takes the URI as its first argument, and the LSP selection range as its - second argument. -* `mdx.toggleEmphasis` — Toggle emphasis syntax at the cursor position. - This takes the URI as its first argument, and the LSP selection range as its - second argument. -* `mdx.toggleInlineCode` — Toggle inline code syntax at the cursor position. - This takes the URI as its first argument, and the LSP selection range as its - second argument. -* `mdx.toggleStrong` — Toggle strong syntax at the cursor position. - This takes the URI as its first argument, and the LSP selection range as its - second argument. +##### `mdx.toggleDelete` + +Toggle delete syntax at the cursor position. +It uses the `workspace/applyEdit` command to apply edits. + +###### Arguments + +* `uri` — The URI of the document to apply changes to. +* `range` — The current selection range of the user. + +###### Returns + +`null` + +##### `mdx.toggleEmphasis` + +Toggle emphasis syntax at the cursor position. +It uses the `workspace/applyEdit` command to apply edits. + +###### Arguments + +* `uri` — The URI of the document to apply changes to. +* `range` — The current selection range of the user. + +###### Returns + +`null` + +##### `mdx.toggleInlineCode` + +Toggle inline code syntax at the cursor position. +It uses the `workspace/applyEdit` command to apply edits. + +###### Arguments + +* `uri` — The URI of the document to apply changes to. +* `range` — The current selection range of the user. + +###### Returns + +`null` + +##### `mdx.toggleStrong` + +Toggle strong syntax at the cursor position. +It uses the `workspace/applyEdit` command to apply edits. + +###### Arguments + +* `uri` — The URI of the document to apply changes to. +* `range` — The current selection range of the user. + +###### Returns + +`null` ### Initialize Options diff --git a/packages/language-service/README.md b/packages/language-service/README.md index 4b55dcef..6fd4efcc 100644 --- a/packages/language-service/README.md +++ b/packages/language-service/README.md @@ -18,7 +18,7 @@ * [Use](#use) * [API](#api) * [`createMdxLanguagePlugin([plugins][, checkMdx][, jsxImportSource])`](#createmdxlanguagepluginplugins-checkmdx-jsximportsource) - * [`createMdxServicePlugin()`](#createmdxserviceplugin) + * [`createMdxServicePlugin(options)`](#createmdxservicepluginoptions) * [`resolveRemarkPlugins(mdxConfig, resolvePlugin)`](#resolveremarkpluginsmdxconfig-resolveplugin) * [Compatibility](#compatibility) * [Types](#types) @@ -103,7 +103,7 @@ Create a [Volar][] language plugin to support [MDX][]. A Volar language plugin to support MDX. -### `createMdxServicePlugin()` +### `createMdxServicePlugin(options)` Create a [Volar][] service module to support [MDX][]. The service supports: @@ -129,7 +129,8 @@ The following commands are supported: #### Parameters -This function doesn’t take any parameters. +* `options` — An object with the following properties: + * `applyEdit` — A function to apply an LSP workspace edit. #### Returns From 08da27ffcb45576a14901b5646cd983d0f972c10 Mon Sep 17 00:00:00 2001 From: Remco Haszing Date: Sun, 8 Sep 2024 16:15:54 +0200 Subject: [PATCH 6/6] Move language-server dependency vscode-uri to dev dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It’s no longer used in production code. --- packages/language-server/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index e7a8f8ce..c5b378de 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -38,12 +38,12 @@ "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.0", "volar-service-markdown": "0.0.61", - "volar-service-typescript": "0.0.61", - "vscode-uri": "^3.0.0" + "volar-service-typescript": "0.0.61" }, "devDependencies": { "@types/node": "^22.0.0", "@volar/test-utils": "~2.4.0", - "unified": "^11.0.0" + "unified": "^11.0.0", + "vscode-uri": "^3.0.0" } }