Skip to content

Commit

Permalink
Create commands to toggle prose syntax (#384)
Browse files Browse the repository at this point in the history
The prose syntax is based on the current text selection and the AST.
If the selection is within range of the syntax that’s toggled, the
syntax is removed. Otherwise the selected text is expanded to include
full words and wrapped.

The supported syntaxes are:

- `delete`
- `emphasis`
- `inlineCode`
- `strong`

Closes #353
  • Loading branch information
remcohaszing committed Jan 18, 2024
1 parent 16a3240 commit b9a910e
Show file tree
Hide file tree
Showing 11 changed files with 414 additions and 29 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-cycles-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"vscode-mdx": minor
---

Support the commands `mdx.toggleDelete`, `mdx.toggleEmphasis`, `mdx.toggleInlineCode`, and `mdx.toggleStrong`.
6 changes: 6 additions & 0 deletions .changeset/real-eels-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@mdx-js/language-service": minor
"@mdx-js/language-server": minor
---

Support the commands `mdx/toggleDelete`, `mdx/toggleEmphasis`, `mdx/toggleInlineCode`, and `mdx/toggleStrong`.
31 changes: 31 additions & 0 deletions packages/language-server/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node

/**
* @typedef {import('@mdx-js/language-service').Commands} Commands
* @typedef {import('unified').PluggableList} PluggableList
* @typedef {import('unified').Plugin} Plugin
*/
Expand Down Expand Up @@ -96,8 +97,38 @@ connection.onInitialize((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(() => {
server.initialized()
})

connection.listen()

/**
* @param {string} uri
* @returns {Promise<Commands>}
*/
async function getCommands(uri) {
const project = await server.projects.getProject(uri)
const service = project.getLanguageService()
return service.context.inject('mdxCommands')
}
4 changes: 4 additions & 0 deletions packages/language-service/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @typedef {import('./lib/service-plugin.js').Commands} Commands
*/

export {createMdxLanguagePlugin} from './lib/language-plugin.js'
export {createMdxServicePlugin} from './lib/service-plugin.js'
export {resolveRemarkPlugins} from './lib/tsconfig.js'
167 changes: 167 additions & 0 deletions packages/language-service/lib/commands.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* @typedef {import('@volar/language-service').Range} Range
* @typedef {import('@volar/language-service').ServiceContext} ServiceContext
* @typedef {import('@volar/language-service').TextEdit} TextEdit
* @typedef {import('mdast').Nodes} Nodes
*/

/**
* @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.
* @returns {TextEdit[] | undefined}
* LSP text edits that should be made.
*/

import {visitParents} from 'unist-util-visit-parents'
import {getNodeEndOffset, getNodeStartOffset} from './mdast-utils.js'
import {VirtualMdxFile} from './virtual-file.js'

/**
* Create a function to toggle prose syntax based on the AST.
*
* @param {ServiceContext} 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.
*/
export function createSyntaxToggle(context, type, separator) {
return ({range, uri}) => {
const file = getVirtualMdxFile(context, uri)

if (!file) {
return
}

const ast = file.ast

if (!ast) {
return
}

const doc = context.documents.get(uri, file.languageId, file.snapshot)
const selectionStart = doc.offsetAt(range.start)
const selectionEnd = doc.offsetAt(range.end)

/** @type {TextEdit[]} */
const edits = []

visitParents(ast, 'text', (node, ancestors) => {
const nodeStart = getNodeStartOffset(node)
const nodeEnd = getNodeEndOffset(node)

if (selectionStart < nodeStart) {
// Outside of this node
return
}

if (selectionEnd > nodeEnd) {
// Outside of this node
return
}

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))
)

edits.push(
{
newText: '',
range: {
start: doc.positionAt(ancestorStart),
end: doc.positionAt(firstChildStart)
}
},
{
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
}

if (matchOffset >= selectionEnd) {
insertEnd = matchOffset
break
}
}

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 (edits) {
return edits
}
}
}

/**
* Get the virtual MDX file that matches a document uri.
*
* @param {ServiceContext} context
* The Volar service context to use.
* @param {string} uri
* The uri of which to find the matching virtual MDX file.
* @returns {VirtualMdxFile | undefined}
* The matching virtual MDX file, if it exists. Otherwise undefined.
*/
export function getVirtualMdxFile(context, uri) {
const [file] = context.language.files.getVirtualFile(
context.env.uriToFileName(uri)
)

if (file instanceof VirtualMdxFile) {
return file
}
}
41 changes: 41 additions & 0 deletions packages/language-service/lib/mdast-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @typedef {import('mdast').Nodes} Nodes
* @typedef {import('unist').Point} Point
* @typedef {import('unist').Position} Position
*/

/**
* Get the offset of a parsed unist point.
*
* @param {Point} point
* The unist point of which to get the offset.
* @returns {number}
* The offset of the unist point.
*/
export function getPointOffset(point) {
return /** @type {number} */ (point.offset)
}

/**
* Get the start offset of a parsed unist point.
*
* @param {Nodes} node
* The unist point of which to get the start offset.
* @returns {number}
* The start offset of the unist point.
*/
export function getNodeStartOffset(node) {
return getPointOffset(/** @type {Position} */ (node.position).start)
}

/**
* Get the end offset of a parsed unist point.
*
* @param {Nodes} node
* The unist point of which to get the end offset.
* @returns {number}
* The end offset of the unist point.
*/
export function getNodeEndOffset(node) {
return getPointOffset(/** @type {Position} */ (node.position).end)
}
54 changes: 33 additions & 21 deletions packages/language-service/lib/service-plugin.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
/**
* @typedef {import('@volar/language-service').ServicePlugin} ServicePlugin
* @typedef {import('@volar/language-service').DataTransferItem} DataTransferItem
* @typedef {import('@volar/language-service').ServicePlugin} ServicePlugin
* @typedef {import('@volar/language-service').ServicePluginInstance<Provide>} ServicePluginInstance
* @typedef {import('./commands.js').SyntaxToggle} SyntaxToggle
*/

/**
* @typedef Commands
* @property {SyntaxToggle} toggleDelete
* @property {SyntaxToggle} toggleEmphasis
* @property {SyntaxToggle} toggleInlineCode
* @property {SyntaxToggle} toggleStrong
*/

/**
* @typedef Provide
* @property {() => Commands} mdxCommands
*/

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 {VirtualMdxFile} from './virtual-file.js'
import {createSyntaxToggle, getVirtualMdxFile} from './commands.js'

// https://github.com/microsoft/vscode/blob/1.83.1/extensions/markdown-language-features/src/languageFeatures/copyFiles/shared.ts#L29-L41
const imageExtensions = new Set([
Expand Down Expand Up @@ -37,8 +52,23 @@ const imageExtensions = new Set([
export function createMdxServicePlugin() {
return {
name: 'mdx',

/**
* @returns {ServicePluginInstance}
*/
create(context) {
return {
provide: {
mdxCommands() {
return {
toggleDelete: createSyntaxToggle(context, 'delete', '~'),
toggleEmphasis: createSyntaxToggle(context, 'emphasis', '_'),
toggleInlineCode: createSyntaxToggle(context, 'inlineCode', '`'),
toggleStrong: createSyntaxToggle(context, 'strong', '**')
}
}
},

async provideDocumentDropEdits(document, position, dataTransfer) {
const documentUri = URI.parse(document.uri)

Expand Down Expand Up @@ -126,7 +156,7 @@ export function createMdxServicePlugin() {
},

provideSemanticDiagnostics(document) {
const file = getVirtualMdxFile(document.uri)
const file = getVirtualMdxFile(context, document.uri)

const error = file?.error

Expand Down Expand Up @@ -154,24 +184,6 @@ export function createMdxServicePlugin() {
}
}
}

/**
* Get the virtual MDX file that matches a document uri.
*
* @param {string} uri
* The uri of which to find the matching virtual MDX file.
* @returns {VirtualMdxFile | undefined}
* The matching virtual MDX file, if it exists. Otherwise undefined.
*/
function getVirtualMdxFile(uri) {
const [file] = context.language.files.getVirtualFile(
context.env.uriToFileName(uri)
)

if (file instanceof VirtualMdxFile) {
return file
}
}
}
}
}

0 comments on commit b9a910e

Please sign in to comment.