Skip to content

Commit 6546923

Browse files
authored
feat: quick fix for adding lang="ts" (#2882)
#2876
1 parent ba9185b commit 6546923

File tree

6 files changed

+245
-108
lines changed

6 files changed

+245
-108
lines changed

.changeset/four-papers-learn.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte-language-server': patch
3+
---
4+
5+
feat: quick fix for adding lang="ts"

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

Lines changed: 110 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
isTextSpanInGeneratedCode,
6060
SnapshotMap
6161
} from './utils';
62+
import { Node } from 'vscode-html-languageservice';
6263

6364
/**
6465
* TODO change this to protocol constant if it's part of the protocol
@@ -701,10 +702,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
701702
),
702703
...this.getSvelteQuickFixes(
703704
lang,
704-
document,
705705
cannotFindNameDiagnostic,
706706
tsDoc,
707-
formatCodeBasis,
708707
userPreferences,
709708
formatCodeSettings
710709
)
@@ -760,8 +759,18 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
760759
lang
761760
);
762761

762+
const addLangCodeAction = this.getAddLangTSCodeAction(
763+
document,
764+
context,
765+
tsDoc,
766+
formatCodeBasis
767+
);
768+
763769
// filter out empty code action
764-
return codeActionsNotFilteredOut.map(({ codeAction }) => codeAction).concat(fixAllActions);
770+
const result = codeActionsNotFilteredOut
771+
.map(({ codeAction }) => codeAction)
772+
.concat(fixAllActions);
773+
return addLangCodeAction ? [addLangCodeAction].concat(result) : result;
765774
}
766775

767776
private async convertAndFixCodeFixAction({
@@ -1128,10 +1137,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
11281137

11291138
private getSvelteQuickFixes(
11301139
lang: ts.LanguageService,
1131-
document: Document,
11321140
cannotFindNameDiagnostics: Diagnostic[],
11331141
tsDoc: DocumentSnapshot,
1134-
formatCodeBasis: FormatCodeBasis,
11351142
userPreferences: ts.UserPreferences,
11361143
formatCodeSettings: ts.FormatCodeSettings
11371144
): CustomFixCannotFindNameInfo[] {
@@ -1141,14 +1148,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
11411148
return [];
11421149
}
11431150

1144-
const typeChecker = program.getTypeChecker();
11451151
const results: CustomFixCannotFindNameInfo[] = [];
1146-
const quote = getQuotePreference(sourceFile, userPreferences);
11471152
const getGlobalCompletion = memoize(() =>
11481153
lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings)
11491154
);
1150-
const [tsMajorStr] = ts.version.split('.');
1151-
const tsSupportHandlerQuickFix = parseInt(tsMajorStr) >= 5;
11521155

11531156
for (const diagnostic of cannotFindNameDiagnostics) {
11541157
const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
@@ -1173,24 +1176,6 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
11731176
);
11741177
}
11751178

1176-
if (!tsSupportHandlerQuickFix) {
1177-
const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(
1178-
document,
1179-
diagnostic
1180-
);
1181-
if (isQuickFixTargetEventHandler) {
1182-
fixes.push(
1183-
...this.getEventHandlerQuickFixes(
1184-
identifier,
1185-
tsDoc,
1186-
typeChecker,
1187-
quote,
1188-
formatCodeBasis
1189-
)
1190-
);
1191-
}
1192-
}
1193-
11941179
if (!fixes.length) {
11951180
continue;
11961181
}
@@ -1225,8 +1210,6 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
12251210
return identifier;
12261211
}
12271212

1228-
// TODO: Remove this in late 2023
1229-
// when most users have upgraded to TS 5.0+
12301213
private getSvelteStoreQuickFixes(
12311214
identifier: ts.Identifier,
12321215
lang: ts.LanguageService,
@@ -1275,101 +1258,121 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
12751258
return flatten(completion.entries.filter((c) => c.name === storeIdentifier).map(toFix));
12761259
}
12771260

1278-
/**
1279-
* Workaround for TypeScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
1280-
* We can remove this once TypeScript doesn't have this limitation.
1281-
*/
1282-
private getEventHandlerQuickFixes(
1283-
identifier: ts.Identifier,
1261+
private getAddLangTSCodeAction(
1262+
document: Document,
1263+
context: CodeActionContext,
12841264
tsDoc: DocumentSnapshot,
1285-
typeChecker: ts.TypeChecker,
1286-
quote: string,
12871265
formatCodeBasis: FormatCodeBasis
1288-
): ts.CodeFixAction[] {
1289-
const type = identifier && typeChecker.getContextualType(identifier);
1266+
) {
1267+
if (tsDoc.scriptKind !== ts.ScriptKind.JS) {
1268+
return;
1269+
}
12901270

1291-
// if it's not union typescript should be able to do it. no need to enhance
1292-
if (!type || !type.isUnion()) {
1293-
return [];
1271+
let hasTSOnlyDiagnostic = false;
1272+
for (const diagnostic of context.diagnostics) {
1273+
const num = Number(diagnostic.code);
1274+
const canOnlyBeUsedInTS = num >= 8004 && num <= 8017;
1275+
if (canOnlyBeUsedInTS) {
1276+
hasTSOnlyDiagnostic = true;
1277+
break;
1278+
}
1279+
}
1280+
if (!hasTSOnlyDiagnostic) {
1281+
return;
12941282
}
12951283

1296-
const nonNullable = type.getNonNullableType();
1284+
if (!document.scriptInfo && !document.moduleScriptInfo) {
1285+
const hasNonTopLevelLang = document.html.roots.some((node) =>
1286+
this.hasLangTsScriptTag(node)
1287+
);
1288+
// Might be because issue with parsing the script tag, so don't suggest adding a new one
1289+
if (hasNonTopLevelLang) {
1290+
return;
1291+
}
12971292

1298-
if (
1299-
!(
1300-
nonNullable.flags & ts.TypeFlags.Object &&
1301-
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
1302-
)
1303-
) {
1304-
return [];
1293+
return CodeAction.create(
1294+
'Add <script lang="ts"> tag',
1295+
{
1296+
documentChanges: [
1297+
{
1298+
textDocument: OptionalVersionedTextDocumentIdentifier.create(
1299+
document.uri,
1300+
null
1301+
),
1302+
edits: [
1303+
{
1304+
range: Range.create(
1305+
Position.create(0, 0),
1306+
Position.create(0, 0)
1307+
),
1308+
newText: '<script lang="ts"></script>' + formatCodeBasis.newLine
1309+
}
1310+
]
1311+
}
1312+
]
1313+
},
1314+
CodeActionKind.QuickFix
1315+
);
13051316
}
13061317

1307-
const signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0];
1318+
const edits = [document.scriptInfo, document.moduleScriptInfo]
1319+
.map((info) => {
1320+
if (!info) {
1321+
return;
1322+
}
13081323

1309-
const parameters = signature.parameters.map((p) => {
1310-
const declaration = p.valueDeclaration ?? p.declarations?.[0];
1311-
const typeString = declaration
1312-
? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
1313-
: '';
1324+
const startTagNameEnd = document.positionAt(info.container.start + 7); // <script
1325+
const existingLangOffset = document
1326+
.getText({
1327+
start: startTagNameEnd,
1328+
end: document.positionAt(info.start)
1329+
})
1330+
.indexOf('lang=');
13141331

1315-
return { name: p.name, typeString };
1316-
});
1332+
if (existingLangOffset !== -1) {
1333+
return;
1334+
}
13171335

1318-
const returnType = typeChecker.typeToString(signature.getReturnType());
1319-
const useJsDoc =
1320-
tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX;
1321-
const parametersText = (
1322-
useJsDoc
1323-
? parameters.map((p) => p.name)
1324-
: parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))
1325-
).join(', ');
1326-
1327-
const jsDoc = useJsDoc
1328-
? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
1329-
: [];
1330-
1331-
const newText = [
1332-
...jsDoc,
1333-
`function ${identifier.text}(${parametersText})${
1334-
useJsDoc || returnType === 'any' ? '' : ': ' + returnType
1335-
} {`,
1336-
formatCodeBasis.indent +
1337-
`throw new Error(${quote}Function not implemented.${quote})` +
1338-
formatCodeBasis.semi,
1339-
'}'
1340-
]
1341-
.map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
1342-
.join('');
1336+
return {
1337+
range: Range.create(startTagNameEnd, startTagNameEnd),
1338+
newText: ' lang="ts"'
1339+
};
1340+
})
1341+
.filter(isNotNullOrUndefined);
13431342

1344-
return [
1345-
{
1346-
description: `Add missing function declaration '${identifier.text}'`,
1347-
fixName: 'fixMissingFunctionDeclaration',
1348-
changes: [
1349-
{
1350-
fileName: tsDoc.filePath,
1351-
textChanges: [
1352-
{
1353-
newText,
1354-
span: { start: 0, length: 0 }
1355-
}
1356-
]
1357-
}
1358-
]
1359-
}
1360-
];
1343+
if (edits.length) {
1344+
return CodeAction.create(
1345+
'Add lang="ts" to <script> tag',
1346+
{
1347+
documentChanges: [
1348+
{
1349+
textDocument: OptionalVersionedTextDocumentIdentifier.create(
1350+
document.uri,
1351+
null
1352+
),
1353+
edits
1354+
}
1355+
]
1356+
},
1357+
CodeActionKind.QuickFix
1358+
);
1359+
}
13611360
}
13621361

1363-
private isQuickFixForEventHandler(document: Document, diagnostic: Diagnostic) {
1364-
const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
1362+
private hasLangTsScriptTag(node: Node): boolean {
13651363
if (
1366-
!htmlNode.attributes ||
1367-
!Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
1364+
node.tag === 'script' &&
1365+
(node.attributes?.lang === '"ts"' || node.attributes?.lang === "'ts'") &&
1366+
node.parent
13681367
) {
1369-
return false;
1368+
return true;
13701369
}
1371-
1372-
return true;
1370+
for (const element of node.children) {
1371+
if (this.hasLangTsScriptTag(element)) {
1372+
return true;
1373+
}
1374+
}
1375+
return false;
13731376
}
13741377

13751378
private async getApplicableRefactors(

0 commit comments

Comments
 (0)