Skip to content

Commit

Permalink
Fix auto-imports
Browse files Browse the repository at this point in the history
This change adds support for auto-imports.

The virtual code now contains an empty import of the JSX runtime. This
import was chosen, because it must exist anyway. This is immediately
followed by an empty code mapping, meaning TypeScript always has a place
to insert auto-imports.

Since JSX components can be injected, they are sometimes prefixed with
`_components.` in the virtual code. To support auto-import completions,
an additional mapping is now made to an expression containing merely the
identifier. As a result, the editor now shows auto-import completions,
unless `MDXProvidedComponents` is defined. I don’t know why the
existence of `MDXProvidedComponents` matters, but this probably matches
the expectation of users anyway.

The auto-imports will not be followed by a blank line. This can lead to
a syntax error in case no other imports exist yet. This is not ideal,
but easy and straight-forward to resolve manually.

Closes #452
  • Loading branch information
remcohaszing committed Jan 3, 2025
1 parent 89920f3 commit c8b5c32
Show file tree
Hide file tree
Showing 3 changed files with 384 additions and 62 deletions.
4 changes: 2 additions & 2 deletions packages/language-server/test/completion.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ test('support completion in ESM', async () => {
original: {
data: {
fileName: fixturePath('node16/completion.mdx'),
offset: 81,
offset: 108,
originalItem: {name: 'Boolean'},
uri: String(
URI.from({
Expand Down Expand Up @@ -110,7 +110,7 @@ test('support completion in JSX', async () => {
original: {
data: {
fileName: fixturePath('node16/completion.mdx'),
offset: 119,
offset: 146,
originalItem: {name: 'Boolean'},
uri: String(
URI.from({
Expand Down
85 changes: 71 additions & 14 deletions packages/language-service/lib/virtual-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const jsPrefix = (
jsxImportSource
) => `${tsCheck ? '// @ts-check\n' : ''}/* @jsxRuntime automatic
@jsxImportSource ${jsxImportSource} */
import '${jsxImportSource}/jsx-runtime'
`

/**
Expand Down Expand Up @@ -294,6 +295,21 @@ function processExports(mdx, node, mapping, esm) {
return esm + '\n'
}

/**
* Pad the generated offsets of a Volar code mapping.
*
* @param {CodeMapping} mapping
* The mapping whose generated offsets to pad.
* @param {number} padding
* The padding to append to the generated offsets.
* @returns {undefined}
*/
function padOffsets(mapping, padding) {
for (let i = 0; i < mapping.generatedOffsets.length; i++) {
mapping.generatedOffsets[i] += padding
}
}

/**
* @param {string} mdx
* @param {Root} ast
Expand All @@ -302,6 +318,13 @@ function processExports(mdx, node, mapping, esm) {
* @returns {VirtualCode[]}
*/
function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
let hasAwait = false
let esm = jsPrefix(checkMdx, jsxImportSource)
let jsx = ''
let jsxVariables = ''
let markdown = ''
let nextMarkdownSourceStart = 0

/** @type {CodeMapping[]} */
const jsMappings = []

Expand All @@ -311,9 +334,11 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
* @type {CodeMapping}
*/
const esmMapping = {
sourceOffsets: [],
generatedOffsets: [],
lengths: [],
// The empty mapping makes sure there’s always a valid mapping to insert
// auto-imports.
sourceOffsets: [0],
generatedOffsets: [esm.length],
lengths: [0],
data: {
completion: true,
format: true,
Expand Down Expand Up @@ -343,6 +368,20 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
}
}

const jsxVariablesMapping = {
sourceOffsets: [],
generatedOffsets: [],
lengths: [],
data: {
completion: true,
format: false,
navigation: true,
semantic: true,
structure: true,
verification: true
}
}

/**
* The Volar mapping that maps all markdown content to the virtual markdown file.
*
Expand All @@ -364,13 +403,6 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {

/** @type {VirtualCode[]} */
const virtualCodes = []

let hasAwait = false
let esm = jsPrefix(checkMdx, jsxImportSource)
let jsx = ''
let markdown = ''
let nextMarkdownSourceStart = 0

const visitors = createVisitors()

for (const child of ast.children) {
Expand Down Expand Up @@ -482,6 +514,17 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {

jsx =
addOffset(jsxMapping, mdx, jsx, newIndex, name.start) + '_components.'
if (node.name && node.type === 'JSXOpeningElement') {
jsxVariables =
addOffset(
jsxVariablesMapping,
mdx,
jsxVariables + '// @ts-ignore\n',
name.start,
name.end
) + '\n'
}

newIndex = name.start
}

Expand Down Expand Up @@ -624,6 +667,16 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
jsx = addOffset(jsxMapping, mdx, jsx + jsxIndent, start, lastIndex)
if (isInjectableComponent(node.name, programScope)) {
jsx += '_components.'
if (node.name) {
jsxVariables =
addOffset(
jsxVariablesMapping,
mdx,
jsxVariables + '// @ts-ignore\n',
lastIndex,
lastIndex + node.name.length
) + '\n'
}
}

if (node.name) {
Expand Down Expand Up @@ -741,12 +794,12 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
updateMarkdownFromOffsets(mdx.length, mdx.length)
esm += componentStart(hasAwait, programScope)

for (let i = 0; i < jsxMapping.generatedOffsets.length; i++) {
jsxMapping.generatedOffsets[i] += esm.length
}

padOffsets(jsxMapping, esm.length)
esm += jsx + componentEnd

padOffsets(jsxVariablesMapping, esm.length)
esm += jsxVariables

if (esmMapping.sourceOffsets.length > 0) {
jsMappings.push(esmMapping)
}
Expand All @@ -755,6 +808,10 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
jsMappings.push(jsxMapping)
}

if (jsxVariablesMapping.sourceOffsets.length > 0) {
jsMappings.push(jsxVariablesMapping)
}

virtualCodes.unshift(
{
id: 'jsx',
Expand Down
Loading

0 comments on commit c8b5c32

Please sign in to comment.