Skip to content

Commit 2e69f94

Browse files
authored
Fix organizing imports (#507)
* Fix organizing imports The organize imports command broke documents where imports are present. This was a regression caused by the logic to support auto imports. Closes #502
1 parent 4ccb234 commit 2e69f94

File tree

6 files changed

+321
-188
lines changed

6 files changed

+321
-188
lines changed

.changeset/silly-loops-wait.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@mdx-js/language-service': patch
3+
'@mdx-js/language-server': patch
4+
'@mdx-js/typescript-plugin': patch
5+
'vscode-mdx': patch
6+
---
7+
8+
Fix organizing imports

fixtures/node16/organize-imports.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
frontmatter: yaml
3+
---
4+
5+
import { useState } from 'react'
6+
import { compile } from '@mdx-js/mdx'
7+
import { unified } from 'unified'
8+
import { createRoot } from 'react-dom/client'
9+
10+
Paragraph text.
11+
12+
export function whatever() {
13+
compile;
14+
useState;
15+
createRoot;
16+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"complexity": "off",
5757
"max-lines": "off",
5858
"max-params": "off",
59+
"no-labels": "off",
5960
"@typescript-eslint/consistent-type-definitions": "off",
6061
"@typescript-eslint/naming-convention": "off",
6162
"unicorn/prevent-abbreviations": "off"
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* @import {LanguageServerHandle} from '@volar/test-utils'
3+
*/
4+
import assert from 'node:assert/strict'
5+
import {afterEach, beforeEach, test} from 'node:test'
6+
import {CodeAction, CodeActionTriggerKind} from '@volar/language-server'
7+
import {createServer, fixturePath, fixtureUri, tsdk} from './utils.js'
8+
9+
/** @type {LanguageServerHandle} */
10+
let serverHandle
11+
12+
beforeEach(async () => {
13+
serverHandle = createServer()
14+
await serverHandle.initialize(fixtureUri('node16'), {
15+
typescript: {enabled: true, tsdk}
16+
})
17+
})
18+
19+
afterEach(() => {
20+
serverHandle.connection.dispose()
21+
})
22+
23+
test('organize imports', async () => {
24+
const {uri} = await serverHandle.openTextDocument(
25+
fixturePath('node16/organize-imports.mdx'),
26+
'mdx'
27+
)
28+
29+
const codeActions = await serverHandle.sendCodeActionsRequest(
30+
uri,
31+
{
32+
start: {line: 6, character: 0},
33+
end: {line: 6, character: 0}
34+
},
35+
{
36+
diagnostics: [],
37+
only: ['source.organizeImports'],
38+
triggerKind: CodeActionTriggerKind.Invoked
39+
}
40+
)
41+
42+
assert.ok(codeActions)
43+
const codeAction = codeActions
44+
.filter((c) => CodeAction.is(c))
45+
.find((c) => c.kind === 'source.organizeImports')
46+
delete codeAction?.data
47+
48+
assert.deepEqual(codeAction, {
49+
diagnostics: [],
50+
edit: {
51+
documentChanges: [
52+
{
53+
edits: [
54+
{
55+
newText:
56+
"import { compile } from '@mdx-js/mdx';\n" +
57+
"import { useState } from 'react';\n" +
58+
"import { createRoot } from 'react-dom/client';\n" +
59+
"import { unified } from 'unified';\n",
60+
range: {
61+
end: {character: 0, line: 5},
62+
start: {character: 0, line: 4}
63+
}
64+
},
65+
{
66+
newText: '',
67+
range: {
68+
end: {character: 0, line: 6},
69+
start: {character: 0, line: 5}
70+
}
71+
},
72+
{
73+
newText: '',
74+
range: {
75+
end: {character: 0, line: 7},
76+
start: {character: 0, line: 6}
77+
}
78+
},
79+
{
80+
newText: '',
81+
range: {
82+
end: {character: 0, line: 8},
83+
start: {character: 0, line: 7}
84+
}
85+
}
86+
],
87+
textDocument: {
88+
uri: fixtureUri('node16/organize-imports.mdx'),
89+
version: null
90+
}
91+
}
92+
]
93+
},
94+
kind: 'source.organizeImports',
95+
title: 'Organize Imports'
96+
})
97+
})

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

Lines changed: 103 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,8 @@ import {isInjectableComponent, isInjectableEstree} from './jsx-utils.js'
2424
* @param {string} jsxImportSource
2525
* The string to use for the JSX import source tag.
2626
*/
27-
const jsPrefix = (
28-
tsCheck,
29-
jsxImportSource
30-
) => `${tsCheck ? '// @ts-check\n' : ''}/* @jsxRuntime automatic
27+
const jsPrefix = (tsCheck, jsxImportSource) => `/* @jsxRuntime automatic
3128
@jsxImportSource ${jsxImportSource} */
32-
import '${jsxImportSource}/jsx-runtime'
3329
`
3430

3531
/**
@@ -216,86 +212,6 @@ function getPropsName(node) {
216212
return 'props'
217213
}
218214

219-
/**
220-
* Process exports of an MDX ESM node.
221-
*
222-
* @param {string} mdx
223-
* The full MDX code to process.
224-
* @param {MdxjsEsm} node
225-
* The MDX ESM node to process.
226-
* @param {CodeMapping} mapping
227-
* The Volar mapping to add offsets to.
228-
* @param {string} esm
229-
* The virtual ESM code up to the point this function was called.
230-
* @returns {string}
231-
* The updated virtual ESM code.
232-
*/
233-
function processExports(mdx, node, mapping, esm) {
234-
const start = node.position?.start?.offset
235-
const end = node.position?.end?.offset
236-
237-
if (start === undefined || end === undefined) {
238-
return esm
239-
}
240-
241-
const body = node.data?.estree?.body
242-
243-
if (!body?.length) {
244-
return addOffset(mapping, mdx, esm, start, end, true)
245-
}
246-
247-
for (const child of body) {
248-
if (child.type === 'ExportDefaultDeclaration') {
249-
const propsName = getPropsName(child)
250-
if (propsName) {
251-
esm += layoutJsDoc(propsName)
252-
}
253-
254-
esm = addOffset(
255-
mapping,
256-
mdx,
257-
esm + '\nconst MDXLayout = ',
258-
child.declaration.start,
259-
child.end,
260-
true
261-
)
262-
continue
263-
}
264-
265-
if (child.type === 'ExportNamedDeclaration' && child.source) {
266-
const {specifiers} = child
267-
for (let index = 0; index < specifiers.length; index++) {
268-
const specifier = specifiers[index]
269-
if (
270-
specifier.local.type === 'Identifier'
271-
? specifier.local.name === 'default'
272-
: specifier.local.value === 'default'
273-
) {
274-
esm = addOffset(mapping, mdx, esm, start, specifier.start)
275-
const nextPosition =
276-
index === specifiers.length - 1
277-
? specifier.end
278-
: mdx.indexOf(',', specifier.end) + 1
279-
return (
280-
addOffset(mapping, mdx, esm, nextPosition, end, true) +
281-
'\nimport {' +
282-
(specifier.exported.type === 'Identifier'
283-
? specifier.exported.name
284-
: JSON.stringify(specifier.exported.value)) +
285-
' as MDXLayout} from ' +
286-
JSON.stringify(child.source.value) +
287-
'\n'
288-
)
289-
}
290-
}
291-
}
292-
293-
esm = addOffset(mapping, mdx, esm, child.start, child.end, true)
294-
}
295-
296-
return esm + '\n'
297-
}
298-
299215
/**
300216
* Pad the generated offsets of a Volar code mapping.
301217
*
@@ -327,6 +243,7 @@ function getEmbeddedCodes(
327243
jsxImportSource
328244
) {
329245
let hasAwait = false
246+
let hasImports = false
330247
let esm = jsPrefix(checkMdx, jsxImportSource)
331248
let jsx = ''
332249
let jsxVariables = ''
@@ -345,9 +262,9 @@ function getEmbeddedCodes(
345262
const esmMapping = {
346263
// The empty mapping makes sure there’s always a valid mapping to insert
347264
// auto-imports.
348-
sourceOffsets: [0],
349-
generatedOffsets: [esm.length],
350-
lengths: [0],
265+
sourceOffsets: [],
266+
generatedOffsets: [],
267+
lengths: [],
351268
data: {
352269
completion: true,
353270
format: true,
@@ -493,6 +410,83 @@ function getEmbeddedCodes(
493410
updateMarkdownFromOffsets(startOffset, endOffset)
494411
}
495412

413+
/**
414+
* Process exports of an MDX ESM node.
415+
*
416+
* @param {MdxjsEsm} node
417+
* The MDX ESM node to process.
418+
* @returns {undefined}
419+
*/
420+
function processExports(node) {
421+
const start = node.position?.start?.offset
422+
const end = node.position?.end?.offset
423+
424+
if (start === undefined || end === undefined) {
425+
return
426+
}
427+
428+
const body = node.data?.estree?.body
429+
430+
if (!body?.length) {
431+
esm = addOffset(esmMapping, mdx, esm, start, end, true)
432+
return
433+
}
434+
435+
bodyLoop: for (const child of body) {
436+
if (child.type === 'ExportDefaultDeclaration') {
437+
const propsName = getPropsName(child)
438+
if (propsName) {
439+
esm += layoutJsDoc(propsName)
440+
}
441+
442+
esm = addOffset(
443+
esmMapping,
444+
mdx,
445+
esm + '\nconst MDXLayout = ',
446+
child.declaration.start,
447+
child.end,
448+
true
449+
)
450+
continue
451+
}
452+
453+
if (child.type === 'ExportNamedDeclaration' && child.source) {
454+
const {specifiers} = child
455+
for (let index = 0; index < specifiers.length; index++) {
456+
const specifier = specifiers[index]
457+
if (
458+
specifier.local.type === 'Identifier'
459+
? specifier.local.name === 'default'
460+
: specifier.local.value === 'default'
461+
) {
462+
esm = addOffset(esmMapping, mdx, esm, start, specifier.start)
463+
const nextPosition =
464+
index === specifiers.length - 1
465+
? specifier.end
466+
: mdx.indexOf(',', specifier.end) + 1
467+
esm =
468+
addOffset(esmMapping, mdx, esm, nextPosition, end, true) +
469+
'\nimport {' +
470+
(specifier.exported.type === 'Identifier'
471+
? specifier.exported.name
472+
: JSON.stringify(specifier.exported.value)) +
473+
' as MDXLayout} from ' +
474+
JSON.stringify(child.source.value)
475+
continue bodyLoop
476+
}
477+
}
478+
}
479+
480+
if (child.type === 'ImportDeclaration') {
481+
hasImports = true
482+
}
483+
484+
esm = addOffset(esmMapping, mdx, esm, child.start, child.end, true)
485+
}
486+
487+
esm += '\n'
488+
}
489+
496490
/**
497491
* @param {Program} program
498492
* @param {number} lastIndex
@@ -663,7 +657,7 @@ function getEmbeddedCodes(
663657

664658
case 'mdxjsEsm': {
665659
updateMarkdownFromNode(node)
666-
esm = processExports(mdx, node, esmMapping, esm)
660+
processExports(node)
667661
break
668662
}
669663

@@ -808,6 +802,26 @@ function getEmbeddedCodes(
808802
esm += '\n' + plugin.finalize() + '\n'
809803
}
810804

805+
let prefix = ''
806+
if (checkMdx) {
807+
prefix += '// @ts-check\n'
808+
}
809+
810+
if (!hasImports) {
811+
prefix += `import '${jsxImportSource}/jsx-runtime'\n`
812+
}
813+
814+
if (prefix) {
815+
padOffsets(esmMapping, prefix.length)
816+
esm = prefix + esm
817+
}
818+
819+
if (!hasImports) {
820+
esmMapping.sourceOffsets.unshift(0)
821+
esmMapping.generatedOffsets.unshift(prefix.length)
822+
esmMapping.lengths.unshift(0)
823+
}
824+
811825
updateMarkdownFromOffsets(mdx.length, mdx.length)
812826
esm += componentStart(hasAwait, programScope)
813827

0 commit comments

Comments
 (0)