Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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,119 @@ 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 it's not union typescript should be able to do it. no need to enhance
if (!type || !type.isUnion()) {
return [];
) {
if (tsDoc.scriptKind !== ts.ScriptKind.JS) {
return;
}

const nonNullable = type.getNonNullableType();

if (
!(
nonNullable.flags & ts.TypeFlags.Object &&
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
)
) {
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 signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0];
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;
}
}

const parameters = signature.parameters.map((p) => {
const declaration = p.valueDeclaration ?? p.declarations?.[0];
const typeString = declaration
? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
: '';
const edits = [document.scriptInfo, document.moduleScriptInfo]
.map((info) => {
if (!info) {
return;
}

return { name: p.name, typeString };
});
const startTagNameEnd = document.positionAt(info.container.start + 7); // <script
const existingLangOffset = document
.getText({
start: startTagNameEnd,
end: document.positionAt(info.start)
})
.indexOf('lang=');
if (existingLangOffset !== -1) {
return {
range: Range.create(startTagNameEnd, startTagNameEnd),
newText: ' lang="ts"'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean to return here? Else we would add another one when one exist already IIUC

Suggested change
return {
range: Range.create(startTagNameEnd, startTagNameEnd),
newText: ' lang="ts"'
return;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was originally going to replace the existing lang attribute. Now that I think about it, we might only be able to do it when it's lang="js", but I don't know if there are any people who actually write that. Don't suggest anything might be better.

};
}
return {
range: Range.create(startTagNameEnd, startTagNameEnd),
newText: ' lang="ts"'
};
})
.filter(isNotNullOrUndefined);

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('');
if (edits.length) {
return CodeAction.create(
'Add lang="ts" to <script> tag',
{
documentChanges: [
{
textDocument: OptionalVersionedTextDocumentIdentifier.create(
document.uri,
null
),
edits
}
]
},
CodeActionKind.QuickFix
);
}

return [
return CodeAction.create(
'Add <script lang="ts"> tag',
{
description: `Add missing function declaration '${identifier.text}'`,
fixName: 'fixMissingFunctionDeclaration',
changes: [
documentChanges: [
{
fileName: tsDoc.filePath,
textChanges: [
textDocument: OptionalVersionedTextDocumentIdentifier.create(
document.uri,
null
),
edits: [
{
newText,
span: { start: 0, length: 0 }
range: Range.create(Position.create(0, 0), Position.create(0, 0)),
newText: '<script lang="ts"></script>' + formatCodeBasis.newLine
}
]
}
]
}
];
},
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