Skip to content

Commit 5ef7acf

Browse files
Jojoshuadummdidumm
andauthored
(feat) enhance Quickfixes to include Svelte Stores (#1789)
#1583 Co-authored-by: Simon H <[email protected]>
1 parent 08639cf commit 5ef7acf

File tree

2 files changed

+172
-75
lines changed

2 files changed

+172
-75
lines changed

packages/language-server/src/plugins/typescript/DocumentSnapshot.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from './utils';
2727

2828
/**
29-
* An error which occured while trying to parse/preprocess the svelte file contents.
29+
* An error which occurred while trying to parse/preprocess the svelte file contents.
3030
*/
3131
export interface ParserError {
3232
message: string;
@@ -311,14 +311,14 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
311311
return this.exportedNames.has(name);
312312
}
313313

314-
svelteNodeAt(postionOrOffset: number | Position): SvelteNode | null {
314+
svelteNodeAt(positionOrOffset: number | Position): SvelteNode | null {
315315
if (!this.htmlAst) {
316316
return null;
317317
}
318318
const offset =
319-
typeof postionOrOffset === 'number'
320-
? postionOrOffset
321-
: this.parent.offsetAt(postionOrOffset);
319+
typeof positionOrOffset === 'number'
320+
? positionOrOffset
321+
: this.parent.offsetAt(positionOrOffset);
322322

323323
let foundNode: SvelteNode | null = null;
324324
walk(this.htmlAst, {

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

Lines changed: 167 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
298298
userPreferences
299299
)
300300
.concat(
301-
this.createElementEventHandlerQuickFix(
301+
await this.getSvelteQuickFixes(
302302
lang,
303303
document,
304304
cannotFoundNameDiagnostic,
@@ -513,37 +513,25 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
513513
);
514514
}
515515

516-
/**
517-
* Workaround for TypesScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
518-
* We can remove this once TypesScript doesn't have this limitation.
519-
*/
520-
private createElementEventHandlerQuickFix(
516+
private async getSvelteQuickFixes(
521517
lang: ts.LanguageService,
522518
document: Document,
523519
diagnostics: Diagnostic[],
524520
tsDoc: DocumentSnapshot,
525521
formatCodeBasis: FormatCodeBasis,
526522
userPreferences: ts.UserPreferences
527-
): ts.CodeFixAction[] {
523+
): Promise<ts.CodeFixAction[]> {
528524
const program = lang.getProgram();
529525
const sourceFile = program?.getSourceFile(tsDoc.filePath);
530526
if (!program || !sourceFile) {
531527
return [];
532528
}
533529

534530
const typeChecker = program.getTypeChecker();
535-
const result: ts.CodeFixAction[] = [];
531+
const results: ts.CodeFixAction[] = [];
536532
const quote = getQuotePreference(sourceFile, userPreferences);
537533

538534
for (const diagnostic of diagnostics) {
539-
const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
540-
if (
541-
!htmlNode.attributes ||
542-
!Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
543-
) {
544-
continue;
545-
}
546-
547535
const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.start));
548536
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.end));
549537

@@ -553,66 +541,165 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
553541
ts.isIdentifier
554542
);
555543

556-
const type = identifier && typeChecker.getContextualType(identifier);
557-
558-
// if it's not union typescript should be able to do it. no need to enhance
559-
if (!type || !type.isUnion()) {
544+
if (!identifier) {
560545
continue;
561546
}
562547

563-
const nonNullable = type.getNonNullableType();
548+
const isQuickFixTargetTargetStore =
549+
identifier?.escapedText.toString().startsWith('$') && diagnostic.code === 2304;
550+
const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(
551+
document,
552+
diagnostic
553+
);
564554

565-
if (
566-
!(
567-
nonNullable.flags & ts.TypeFlags.Object &&
568-
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
569-
)
570-
) {
571-
continue;
555+
if (isQuickFixTargetTargetStore) {
556+
results.push(
557+
...(await this.getSvelteStoreQuickFixes(
558+
identifier,
559+
lang,
560+
document,
561+
tsDoc,
562+
userPreferences
563+
))
564+
);
572565
}
573566

574-
const signature = typeChecker.getSignaturesOfType(
575-
nonNullable,
576-
ts.SignatureKind.Call
577-
)[0];
567+
if (isQuickFixTargetEventHandler) {
568+
results.push(
569+
...this.getEventHandlerQuickFixes(
570+
identifier,
571+
tsDoc,
572+
typeChecker,
573+
quote,
574+
formatCodeBasis
575+
)
576+
);
577+
}
578+
}
578579

579-
const parameters = signature.parameters.map((p) => {
580-
const declaration = p.valueDeclaration ?? p.declarations?.[0];
581-
const typeString = declaration
582-
? typeChecker.typeToString(
583-
typeChecker.getTypeOfSymbolAtLocation(p, declaration)
584-
)
585-
: '';
580+
return results;
581+
}
586582

587-
return { name: p.name, typeString };
588-
});
589-
const returnType = typeChecker.typeToString(signature.getReturnType());
590-
const useJsDoc =
591-
tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX;
592-
const parametersText = (
593-
useJsDoc
594-
? parameters.map((p) => p.name)
595-
: parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))
596-
).join(', ');
597-
598-
const jsDoc = useJsDoc
599-
? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
600-
: [];
601-
602-
const newText = [
603-
...jsDoc,
604-
`function ${identifier.text}(${parametersText})${
605-
useJsDoc ? '' : ': ' + returnType
606-
} {`,
607-
formatCodeBasis.indent +
608-
`throw new Error(${quote}Function not implemented.${quote})` +
609-
formatCodeBasis.semi,
610-
'}'
611-
]
612-
.map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
613-
.join('');
614-
615-
result.push({
583+
private async getSvelteStoreQuickFixes(
584+
identifier: ts.Identifier,
585+
lang: ts.LanguageService,
586+
document: Document,
587+
tsDoc: DocumentSnapshot,
588+
userPreferences: ts.UserPreferences
589+
): Promise<ts.CodeFixAction[]> {
590+
const storeIdentifier = identifier.escapedText.toString().substring(1);
591+
const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(
592+
document,
593+
tsDoc.scriptKind
594+
);
595+
const completion = lang.getCompletionsAtPosition(
596+
tsDoc.filePath,
597+
0,
598+
userPreferences,
599+
formatCodeSettings
600+
);
601+
602+
if (!completion) {
603+
return [];
604+
}
605+
606+
const toFix = (c: ts.CompletionEntry) =>
607+
lang
608+
.getCompletionEntryDetails(
609+
tsDoc.filePath,
610+
0,
611+
c.name,
612+
formatCodeSettings,
613+
c.source,
614+
userPreferences,
615+
c.data
616+
)
617+
?.codeActions?.map((a) => ({
618+
...a,
619+
changes: a.changes.map((change) => {
620+
return {
621+
...change,
622+
textChanges: change.textChanges.map((textChange) => {
623+
// For some reason, TS sometimes adds the `type` modifier. Remove it.
624+
return {
625+
...textChange,
626+
newText: textChange.newText.replace(' type ', ' ')
627+
};
628+
})
629+
};
630+
}),
631+
fixName: 'import'
632+
})) ?? [];
633+
634+
return flatten(completion.entries.filter((c) => c.name === storeIdentifier).map(toFix));
635+
}
636+
637+
/**
638+
* Workaround for TypeScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
639+
* We can remove this once TypeScript doesn't have this limitation.
640+
*/
641+
private getEventHandlerQuickFixes(
642+
identifier: ts.Identifier,
643+
tsDoc: DocumentSnapshot,
644+
typeChecker: ts.TypeChecker,
645+
quote: string,
646+
formatCodeBasis: FormatCodeBasis
647+
): ts.CodeFixAction[] {
648+
const type = identifier && typeChecker.getContextualType(identifier);
649+
650+
// if it's not union typescript should be able to do it. no need to enhance
651+
if (!type || !type.isUnion()) {
652+
return [];
653+
}
654+
655+
const nonNullable = type.getNonNullableType();
656+
657+
if (
658+
!(
659+
nonNullable.flags & ts.TypeFlags.Object &&
660+
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
661+
)
662+
) {
663+
return [];
664+
}
665+
666+
const signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0];
667+
668+
const parameters = signature.parameters.map((p) => {
669+
const declaration = p.valueDeclaration ?? p.declarations?.[0];
670+
const typeString = declaration
671+
? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
672+
: '';
673+
674+
return { name: p.name, typeString };
675+
});
676+
677+
const returnType = typeChecker.typeToString(signature.getReturnType());
678+
const useJsDoc =
679+
tsDoc.scriptKind === ts.ScriptKind.JS || tsDoc.scriptKind === ts.ScriptKind.JSX;
680+
const parametersText = (
681+
useJsDoc
682+
? parameters.map((p) => p.name)
683+
: parameters.map((p) => p.name + (p.typeString ? ': ' + p.typeString : ''))
684+
).join(', ');
685+
686+
const jsDoc = useJsDoc
687+
? ['/**', ...parameters.map((p) => ` * @param {${p.typeString}} ${p.name}`), ' */']
688+
: [];
689+
690+
const newText = [
691+
...jsDoc,
692+
`function ${identifier.text}(${parametersText})${useJsDoc ? '' : ': ' + returnType} {`,
693+
formatCodeBasis.indent +
694+
`throw new Error(${quote}Function not implemented.${quote})` +
695+
formatCodeBasis.semi,
696+
'}'
697+
]
698+
.map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
699+
.join('');
700+
701+
return [
702+
{
616703
description: `Add missing function declaration '${identifier.text}'`,
617704
fixName: 'fixMissingFunctionDeclaration',
618705
changes: [
@@ -626,10 +713,20 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
626713
]
627714
}
628715
]
629-
});
716+
}
717+
];
718+
}
719+
720+
private isQuickFixForEventHandler(document: Document, diagnostic: Diagnostic) {
721+
const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
722+
if (
723+
!htmlNode.attributes ||
724+
!Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
725+
) {
726+
return false;
630727
}
631728

632-
return result;
729+
return true;
633730
}
634731

635732
private async getApplicableRefactors(

0 commit comments

Comments
 (0)