Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/four-papers-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte-language-server': patch
---

feat: quick fix for adding lang="ts"
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import {
isTextSpanInGeneratedCode,
SnapshotMap
} from './utils';
import { Node } from 'vscode-html-languageservice';

/**
* TODO change this to protocol constant if it's part of the protocol
Expand Down Expand Up @@ -701,10 +702,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
),
...this.getSvelteQuickFixes(
lang,
document,
cannotFindNameDiagnostic,
tsDoc,
formatCodeBasis,
userPreferences,
formatCodeSettings
)
Expand Down Expand Up @@ -760,8 +759,18 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
lang
);

const addLangCodeAction = this.getAddLangTSCodeAction(
document,
context,
tsDoc,
formatCodeBasis
);

// filter out empty code action
return codeActionsNotFilteredOut.map(({ codeAction }) => codeAction).concat(fixAllActions);
const result = codeActionsNotFilteredOut
.map(({ codeAction }) => codeAction)
.concat(fixAllActions);
return addLangCodeAction ? [addLangCodeAction].concat(result) : result;
}

private async convertAndFixCodeFixAction({
Expand Down Expand Up @@ -1128,10 +1137,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {

private getSvelteQuickFixes(
lang: ts.LanguageService,
document: Document,
cannotFindNameDiagnostics: Diagnostic[],
tsDoc: DocumentSnapshot,
formatCodeBasis: FormatCodeBasis,
userPreferences: ts.UserPreferences,
formatCodeSettings: ts.FormatCodeSettings
): CustomFixCannotFindNameInfo[] {
Expand All @@ -1141,14 +1148,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
return [];
}

const typeChecker = program.getTypeChecker();
const results: CustomFixCannotFindNameInfo[] = [];
const quote = getQuotePreference(sourceFile, userPreferences);
const getGlobalCompletion = memoize(() =>
lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings)
);
const [tsMajorStr] = ts.version.split('.');
const tsSupportHandlerQuickFix = parseInt(tsMajorStr) >= 5;

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

if (!tsSupportHandlerQuickFix) {
const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(
document,
diagnostic
);
if (isQuickFixTargetEventHandler) {
fixes.push(
...this.getEventHandlerQuickFixes(
identifier,
tsDoc,
typeChecker,
quote,
formatCodeBasis
)
);
}
}

if (!fixes.length) {
continue;
}
Expand Down Expand Up @@ -1225,8 +1210,6 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
return identifier;
}

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

/**
* Workaround for TypeScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
* We can remove this once TypeScript doesn't have this limitation.
*/
private getEventHandlerQuickFixes(
identifier: ts.Identifier,
private getAddLangTSCodeAction(
document: Document,
context: CodeActionContext,
tsDoc: DocumentSnapshot,
typeChecker: ts.TypeChecker,
quote: string,
formatCodeBasis: FormatCodeBasis
): ts.CodeFixAction[] {
const type = identifier && typeChecker.getContextualType(identifier);
) {
if (tsDoc.scriptKind !== ts.ScriptKind.JS) {
return;
}

// if it's not union typescript should be able to do it. no need to enhance
if (!type || !type.isUnion()) {
return [];
let hasTSOnlyDiagnostic = false;
for (const diagnostic of context.diagnostics) {
const num = Number(diagnostic.code);
const canOnlyBeUsedInTS = num >= 8004 && num <= 8017;
if (canOnlyBeUsedInTS) {
hasTSOnlyDiagnostic = true;
break;
}
}
if (!hasTSOnlyDiagnostic) {
return;
}

const nonNullable = type.getNonNullableType();
if (!document.scriptInfo && !document.moduleScriptInfo) {
const hasNonTopLevelLang = document.html.roots.some((node) =>
this.hasLangTsScriptTag(node)
);
// Might be because issue with parsing the script tag, so don't suggest adding a new one
if (hasNonTopLevelLang) {
return;
}

if (
!(
nonNullable.flags & ts.TypeFlags.Object &&
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
)
) {
return [];
return CodeAction.create(
'Add <script lang="ts"> tag',
{
documentChanges: [
{
textDocument: OptionalVersionedTextDocumentIdentifier.create(
document.uri,
null
),
edits: [
{
range: Range.create(
Position.create(0, 0),
Position.create(0, 0)
),
newText: '<script lang="ts"></script>' + formatCodeBasis.newLine
}
]
}
]
},
CodeActionKind.QuickFix
);
}

const signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0];
const edits = [document.scriptInfo, document.moduleScriptInfo]
.map((info) => {
if (!info) {
return;
}

const parameters = signature.parameters.map((p) => {
const declaration = p.valueDeclaration ?? p.declarations?.[0];
const typeString = declaration
? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
: '';
const startTagNameEnd = document.positionAt(info.container.start + 7); // <script
const existingLangOffset = document
.getText({
start: startTagNameEnd,
end: document.positionAt(info.start)
})
.indexOf('lang=');

return { name: p.name, typeString };
});
if (existingLangOffset !== -1) {
return;
}

const returnType = typeChecker.typeToString(signature.getReturnType());
const useJsDoc =
tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX;
const parametersText = (
useJsDoc
? parameters.map((p) => p.name)
: parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))
).join(', ');

const jsDoc = useJsDoc
? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
: [];

const newText = [
...jsDoc,
`function ${identifier.text}(${parametersText})${
useJsDoc || returnType === 'any' ? '' : ': ' + returnType
} {`,
formatCodeBasis.indent +
`throw new Error(${quote}Function not implemented.${quote})` +
formatCodeBasis.semi,
'}'
]
.map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
.join('');
return {
range: Range.create(startTagNameEnd, startTagNameEnd),
newText: ' lang="ts"'
};
})
.filter(isNotNullOrUndefined);

return [
{
description: `Add missing function declaration '${identifier.text}'`,
fixName: 'fixMissingFunctionDeclaration',
changes: [
{
fileName: tsDoc.filePath,
textChanges: [
{
newText,
span: { start: 0, length: 0 }
}
]
}
]
}
];
if (edits.length) {
return CodeAction.create(
'Add lang="ts" to <script> tag',
{
documentChanges: [
{
textDocument: OptionalVersionedTextDocumentIdentifier.create(
document.uri,
null
),
edits
}
]
},
CodeActionKind.QuickFix
);
}
}

private isQuickFixForEventHandler(document: Document, diagnostic: Diagnostic) {
const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
private hasLangTsScriptTag(node: Node): boolean {
if (
!htmlNode.attributes ||
!Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
node.tag === 'script' &&
(node.attributes?.lang === '"ts"' || node.attributes?.lang === "'ts'") &&
node.parent
) {
return false;
return true;
}

return true;
for (const element of node.children) {
if (this.hasLangTsScriptTag(element)) {
return true;
}
}
return false;
}

private async getApplicableRefactors(
Expand Down
Loading