Skip to content

Commit c8b5c32

Browse files
committed
Fix auto-imports
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
1 parent 89920f3 commit c8b5c32

File tree

3 files changed

+384
-62
lines changed

3 files changed

+384
-62
lines changed

packages/language-server/test/completion.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ test('support completion in ESM', async () => {
4747
original: {
4848
data: {
4949
fileName: fixturePath('node16/completion.mdx'),
50-
offset: 81,
50+
offset: 108,
5151
originalItem: {name: 'Boolean'},
5252
uri: String(
5353
URI.from({
@@ -110,7 +110,7 @@ test('support completion in JSX', async () => {
110110
original: {
111111
data: {
112112
fileName: fixturePath('node16/completion.mdx'),
113-
offset: 119,
113+
offset: 146,
114114
originalItem: {name: 'Boolean'},
115115
uri: String(
116116
URI.from({

packages/language-service/lib/virtual-code.js

+71-14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const jsPrefix = (
2828
jsxImportSource
2929
) => `${tsCheck ? '// @ts-check\n' : ''}/* @jsxRuntime automatic
3030
@jsxImportSource ${jsxImportSource} */
31+
import '${jsxImportSource}/jsx-runtime'
3132
`
3233

3334
/**
@@ -294,6 +295,21 @@ function processExports(mdx, node, mapping, esm) {
294295
return esm + '\n'
295296
}
296297

298+
/**
299+
* Pad the generated offsets of a Volar code mapping.
300+
*
301+
* @param {CodeMapping} mapping
302+
* The mapping whose generated offsets to pad.
303+
* @param {number} padding
304+
* The padding to append to the generated offsets.
305+
* @returns {undefined}
306+
*/
307+
function padOffsets(mapping, padding) {
308+
for (let i = 0; i < mapping.generatedOffsets.length; i++) {
309+
mapping.generatedOffsets[i] += padding
310+
}
311+
}
312+
297313
/**
298314
* @param {string} mdx
299315
* @param {Root} ast
@@ -302,6 +318,13 @@ function processExports(mdx, node, mapping, esm) {
302318
* @returns {VirtualCode[]}
303319
*/
304320
function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
321+
let hasAwait = false
322+
let esm = jsPrefix(checkMdx, jsxImportSource)
323+
let jsx = ''
324+
let jsxVariables = ''
325+
let markdown = ''
326+
let nextMarkdownSourceStart = 0
327+
305328
/** @type {CodeMapping[]} */
306329
const jsMappings = []
307330

@@ -311,9 +334,11 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
311334
* @type {CodeMapping}
312335
*/
313336
const esmMapping = {
314-
sourceOffsets: [],
315-
generatedOffsets: [],
316-
lengths: [],
337+
// The empty mapping makes sure there’s always a valid mapping to insert
338+
// auto-imports.
339+
sourceOffsets: [0],
340+
generatedOffsets: [esm.length],
341+
lengths: [0],
317342
data: {
318343
completion: true,
319344
format: true,
@@ -343,6 +368,20 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
343368
}
344369
}
345370

371+
const jsxVariablesMapping = {
372+
sourceOffsets: [],
373+
generatedOffsets: [],
374+
lengths: [],
375+
data: {
376+
completion: true,
377+
format: false,
378+
navigation: true,
379+
semantic: true,
380+
structure: true,
381+
verification: true
382+
}
383+
}
384+
346385
/**
347386
* The Volar mapping that maps all markdown content to the virtual markdown file.
348387
*
@@ -364,13 +403,6 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
364403

365404
/** @type {VirtualCode[]} */
366405
const virtualCodes = []
367-
368-
let hasAwait = false
369-
let esm = jsPrefix(checkMdx, jsxImportSource)
370-
let jsx = ''
371-
let markdown = ''
372-
let nextMarkdownSourceStart = 0
373-
374406
const visitors = createVisitors()
375407

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

483515
jsx =
484516
addOffset(jsxMapping, mdx, jsx, newIndex, name.start) + '_components.'
517+
if (node.name && node.type === 'JSXOpeningElement') {
518+
jsxVariables =
519+
addOffset(
520+
jsxVariablesMapping,
521+
mdx,
522+
jsxVariables + '// @ts-ignore\n',
523+
name.start,
524+
name.end
525+
) + '\n'
526+
}
527+
485528
newIndex = name.start
486529
}
487530

@@ -624,6 +667,16 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
624667
jsx = addOffset(jsxMapping, mdx, jsx + jsxIndent, start, lastIndex)
625668
if (isInjectableComponent(node.name, programScope)) {
626669
jsx += '_components.'
670+
if (node.name) {
671+
jsxVariables =
672+
addOffset(
673+
jsxVariablesMapping,
674+
mdx,
675+
jsxVariables + '// @ts-ignore\n',
676+
lastIndex,
677+
lastIndex + node.name.length
678+
) + '\n'
679+
}
627680
}
628681

629682
if (node.name) {
@@ -741,12 +794,12 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
741794
updateMarkdownFromOffsets(mdx.length, mdx.length)
742795
esm += componentStart(hasAwait, programScope)
743796

744-
for (let i = 0; i < jsxMapping.generatedOffsets.length; i++) {
745-
jsxMapping.generatedOffsets[i] += esm.length
746-
}
747-
797+
padOffsets(jsxMapping, esm.length)
748798
esm += jsx + componentEnd
749799

800+
padOffsets(jsxVariablesMapping, esm.length)
801+
esm += jsxVariables
802+
750803
if (esmMapping.sourceOffsets.length > 0) {
751804
jsMappings.push(esmMapping)
752805
}
@@ -755,6 +808,10 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
755808
jsMappings.push(jsxMapping)
756809
}
757810

811+
if (jsxVariablesMapping.sourceOffsets.length > 0) {
812+
jsMappings.push(jsxVariablesMapping)
813+
}
814+
758815
virtualCodes.unshift(
759816
{
760817
id: 'jsx',

0 commit comments

Comments
 (0)