Skip to content

Commit 5366e78

Browse files
authored
feat: support "add missing imports on save" (#2744)
Fixes #2616 Adds the source action to addMissingImports, reusing the same quick fix action as "import all missing"
1 parent ddc62b8 commit 5366e78

File tree

5 files changed

+161
-9
lines changed

5 files changed

+161
-9
lines changed

packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { internalHelpers } from 'svelte2tsx';
12
import ts from 'typescript';
23
import {
34
CancellationToken,
@@ -24,6 +25,7 @@ import {
2425
} from '../../../lib/documents';
2526
import { LSConfigManager } from '../../../ls-config';
2627
import {
28+
createGetCanonicalFileName,
2729
flatten,
2830
getIndent,
2931
isNotNullOrUndefined,
@@ -37,13 +39,15 @@ import {
3739
import { CodeActionsProvider } from '../../interfaces';
3840
import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot';
3941
import { LSAndTSDocResolver } from '../LSAndTSDocResolver';
42+
import { LanguageServiceContainer } from '../service';
4043
import {
4144
changeSvelteComponentName,
4245
convertRange,
4346
isInScript,
4447
toGeneratedSvelteComponentName
4548
} from '../utils';
4649
import { CompletionsProviderImpl } from './CompletionProvider';
50+
import { DiagnosticCode } from './DiagnosticsProvider';
4751
import {
4852
findClosestContainingNode,
4953
FormatCodeBasis,
@@ -53,15 +57,12 @@ import {
5357
isTextSpanInGeneratedCode,
5458
SnapshotMap
5559
} from './utils';
56-
import { DiagnosticCode } from './DiagnosticsProvider';
57-
import { createGetCanonicalFileName } from '../../../utils';
58-
import { LanguageServiceContainer } from '../service';
59-
import { internalHelpers } from 'svelte2tsx';
6060

6161
/**
6262
* TODO change this to protocol constant if it's part of the protocol
6363
*/
6464
export const SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports';
65+
export const ADD_MISSING_IMPORTS_CODE_ACTION_KIND = 'source.addMissingImports';
6566

6667
interface RefactorArgs {
6768
type: 'refactor';
@@ -121,6 +122,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
121122
);
122123
}
123124

125+
if (context.only?.[0] === ADD_MISSING_IMPORTS_CODE_ACTION_KIND) {
126+
return await this.addMissingImports(document, cancellationToken);
127+
}
128+
124129
// for source action command (all source.xxx)
125130
// vscode would show different source code action kinds to choose from
126131
if (context.only?.[0] === CodeActionKind.Source) {
@@ -130,7 +135,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
130135
document,
131136
cancellationToken,
132137
/**skipDestructiveCodeActions */ true
133-
))
138+
)),
139+
...(await this.addMissingImports(document, cancellationToken))
134140
];
135141
}
136142

@@ -1553,4 +1559,48 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
15531559
private async getLSAndTSDoc(document: Document) {
15541560
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
15551561
}
1562+
1563+
private async addMissingImports(
1564+
document: Document,
1565+
cancellationToken?: CancellationToken
1566+
): Promise<CodeAction[]> {
1567+
// Re-introduce LS/TSDoc resolution and diagnostic check
1568+
const { lang, tsDoc } = await this.getLSAndTSDoc(document);
1569+
if (cancellationToken?.isCancellationRequested) {
1570+
return [];
1571+
}
1572+
1573+
// Check if there are any relevant "cannot find name" diagnostics
1574+
const diagnostics = lang.getSemanticDiagnostics(tsDoc.filePath);
1575+
const hasMissingImports = diagnostics.some(
1576+
(diag) =>
1577+
(diag.code === DiagnosticCode.CANNOT_FIND_NAME ||
1578+
diag.code === DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y) &&
1579+
// Ensure the diagnostic is not in generated code
1580+
!isTextSpanInGeneratedCode(tsDoc.getFullText(), {
1581+
start: diag.start ?? 0,
1582+
length: diag.length ?? 0
1583+
})
1584+
);
1585+
1586+
// Only return the action if there are potential imports to add
1587+
if (!hasMissingImports) {
1588+
return [];
1589+
}
1590+
1591+
// If imports might be needed, create the deferred action
1592+
const codeAction = CodeAction.create(
1593+
FIX_IMPORT_FIX_DESCRIPTION,
1594+
ADD_MISSING_IMPORTS_CODE_ACTION_KIND
1595+
);
1596+
1597+
const data: QuickFixAllResolveInfo = {
1598+
uri: document.uri,
1599+
fixName: FIX_IMPORT_FIX_NAME,
1600+
fixId: FIX_IMPORT_FIX_ID
1601+
};
1602+
codeAction.data = data;
1603+
1604+
return [codeAction];
1605+
}
15561606
}

packages/language-server/src/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ import { debounceThrottle, isNotNullOrUndefined, normalizeUri, urlToPath } from
4545
import { FallbackWatcher } from './lib/FallbackWatcher';
4646
import { configLoader } from './lib/documents/configLoader';
4747
import { setIsTrusted } from './importPackage';
48-
import { SORT_IMPORT_CODE_ACTION_KIND } from './plugins/typescript/features/CodeActionsProvider';
48+
import {
49+
SORT_IMPORT_CODE_ACTION_KIND,
50+
ADD_MISSING_IMPORTS_CODE_ACTION_KIND
51+
} from './plugins/typescript/features/CodeActionsProvider';
4952
import { createLanguageServices } from './plugins/css/service';
5053
import { FileSystemProvider } from './plugins/css/FileSystemProvider';
5154

@@ -270,6 +273,7 @@ export function startServer(options?: LSOptions) {
270273
CodeActionKind.QuickFix,
271274
CodeActionKind.SourceOrganizeImports,
272275
SORT_IMPORT_CODE_ACTION_KIND,
276+
ADD_MISSING_IMPORTS_CODE_ACTION_KIND,
273277
...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : [])
274278
].filter(
275279
clientSupportedCodeActionKinds &&

packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import * as assert from 'assert';
22
import * as path from 'path';
3+
import { VERSION } from 'svelte/compiler';
4+
import { internalHelpers } from 'svelte2tsx';
35
import ts from 'typescript';
46
import {
57
CancellationTokenSource,
@@ -12,17 +14,16 @@ import {
1214
import { Document, DocumentManager } from '../../../../src/lib/documents';
1315
import { LSConfigManager } from '../../../../src/ls-config';
1416
import {
17+
ADD_MISSING_IMPORTS_CODE_ACTION_KIND,
1518
CodeActionsProviderImpl,
1619
SORT_IMPORT_CODE_ACTION_KIND
1720
} from '../../../../src/plugins/typescript/features/CodeActionsProvider';
1821
import { CompletionsProviderImpl } from '../../../../src/plugins/typescript/features/CompletionProvider';
22+
import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider';
1923
import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDocResolver';
2024
import { __resetCache } from '../../../../src/plugins/typescript/service';
2125
import { pathToUrl } from '../../../../src/utils';
2226
import { recursiveServiceWarmup } from '../test-utils';
23-
import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider';
24-
import { VERSION } from 'svelte/compiler';
25-
import { internalHelpers } from 'svelte2tsx';
2627

2728
const testDir = path.join(__dirname, '..');
2829
const indent = ' '.repeat(4);
@@ -2229,4 +2230,84 @@ describe('CodeActionsProvider', function () {
22292230
after(() => {
22302231
__resetCache();
22312232
});
2233+
2234+
it('provides source action for adding all missing imports', async () => {
2235+
const { provider, document } = setup('codeaction-custom-fix-all-component5.svelte');
2236+
2237+
const range = Range.create(Position.create(4, 1), Position.create(4, 15));
2238+
2239+
// Request the specific source action
2240+
const codeActions = await provider.getCodeActions(document, range, {
2241+
diagnostics: [], // Diagnostics might not be needed here if we only want the source action by kind
2242+
only: [ADD_MISSING_IMPORTS_CODE_ACTION_KIND]
2243+
});
2244+
2245+
assert.ok(codeActions.length > 0, 'No code actions found');
2246+
2247+
// Find the action by its kind
2248+
const addImportsAction = codeActions.find((action) => action.data);
2249+
2250+
// Ensure the action was found and has data (as it's now deferred)
2251+
assert.ok(addImportsAction, 'Add missing imports action should be found');
2252+
assert.ok(
2253+
addImportsAction.data,
2254+
'Add missing imports action should have data for resolution'
2255+
);
2256+
2257+
// Resolve the action to get the edits
2258+
const resolvedAction = await provider.resolveCodeAction(document, addImportsAction);
2259+
2260+
// Assert the edits on the resolved action
2261+
assert.ok(resolvedAction.edit, 'Resolved action should have an edit');
2262+
(<TextDocumentEdit>resolvedAction.edit?.documentChanges?.[0])?.edits.forEach(
2263+
(edit) => (edit.newText = harmonizeNewLines(edit.newText))
2264+
);
2265+
2266+
assert.deepStrictEqual(resolvedAction.edit, {
2267+
documentChanges: [
2268+
{
2269+
edits: [
2270+
{
2271+
newText:
2272+
`\n${indent}import FixAllImported from \"./importing/FixAllImported.svelte\";\n` +
2273+
`${indent}import FixAllImported2 from \"./importing/FixAllImported2.svelte\";\n`,
2274+
range: {
2275+
start: {
2276+
character: 18,
2277+
line: 0
2278+
},
2279+
end: {
2280+
character: 18,
2281+
line: 0
2282+
}
2283+
}
2284+
}
2285+
],
2286+
textDocument: {
2287+
uri: getUri('codeaction-custom-fix-all-component5.svelte'),
2288+
version: null
2289+
}
2290+
}
2291+
]
2292+
});
2293+
2294+
// Optional: Verify the kind and title remain correct on the resolved action
2295+
assert.strictEqual(resolvedAction.kind, ADD_MISSING_IMPORTS_CODE_ACTION_KIND);
2296+
assert.strictEqual(resolvedAction.title, 'Add all missing imports');
2297+
});
2298+
2299+
it('provides source action for adding all missing imports only when imports are missing', async () => {
2300+
const { provider, document } = setup('codeaction-custom-fix-all-component6.svelte');
2301+
2302+
const codeActions = await provider.getCodeActions(
2303+
document,
2304+
Range.create(Position.create(1, 4), Position.create(1, 5)),
2305+
{
2306+
diagnostics: [], // No diagnostics = no missing imports
2307+
only: [ADD_MISSING_IMPORTS_CODE_ACTION_KIND]
2308+
}
2309+
);
2310+
2311+
assert.deepStrictEqual(codeActions, []);
2312+
});
22322313
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script lang="ts">
2+
3+
</script>
4+
5+
<FixAllImported />
6+
<FixAllImported2 />
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<script lang="ts">
2+
import FixAllImported from './importing/FixAllImported.svelte';
3+
import FixAllImported2 from './importing/FixAllImported2.svelte';
4+
5+
6+
</script>
7+
8+
<FixAllImported />
9+
<FixAllImported2 />
10+

0 commit comments

Comments
 (0)