Skip to content

Commit

Permalink
(feat) enhance Quickfixes to include Svelte Stores (#1789)
Browse files Browse the repository at this point in the history
#1583

Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
  • Loading branch information
Jojoshua and dummdidumm committed Dec 20, 2022
1 parent 08639cf commit 5ef7acf
Show file tree
Hide file tree
Showing 2 changed files with 172 additions and 75 deletions.
Expand Up @@ -26,7 +26,7 @@ import {
} from './utils';

/**
* An error which occured while trying to parse/preprocess the svelte file contents.
* An error which occurred while trying to parse/preprocess the svelte file contents.
*/
export interface ParserError {
message: string;
Expand Down Expand Up @@ -311,14 +311,14 @@ export class SvelteDocumentSnapshot implements DocumentSnapshot {
return this.exportedNames.has(name);
}

svelteNodeAt(postionOrOffset: number | Position): SvelteNode | null {
svelteNodeAt(positionOrOffset: number | Position): SvelteNode | null {
if (!this.htmlAst) {
return null;
}
const offset =
typeof postionOrOffset === 'number'
? postionOrOffset
: this.parent.offsetAt(postionOrOffset);
typeof positionOrOffset === 'number'
? positionOrOffset
: this.parent.offsetAt(positionOrOffset);

let foundNode: SvelteNode | null = null;
walk(this.htmlAst, {
Expand Down
Expand Up @@ -298,7 +298,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
userPreferences
)
.concat(
this.createElementEventHandlerQuickFix(
await this.getSvelteQuickFixes(
lang,
document,
cannotFoundNameDiagnostic,
Expand Down Expand Up @@ -513,37 +513,25 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
);
}

/**
* Workaround for TypesScript doesn't provide a quick fix if the signature is typed as union type, like `(() => void) | null`
* We can remove this once TypesScript doesn't have this limitation.
*/
private createElementEventHandlerQuickFix(
private async getSvelteQuickFixes(
lang: ts.LanguageService,
document: Document,
diagnostics: Diagnostic[],
tsDoc: DocumentSnapshot,
formatCodeBasis: FormatCodeBasis,
userPreferences: ts.UserPreferences
): ts.CodeFixAction[] {
): Promise<ts.CodeFixAction[]> {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(tsDoc.filePath);
if (!program || !sourceFile) {
return [];
}

const typeChecker = program.getTypeChecker();
const result: ts.CodeFixAction[] = [];
const results: ts.CodeFixAction[] = [];
const quote = getQuotePreference(sourceFile, userPreferences);

for (const diagnostic of diagnostics) {
const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
if (
!htmlNode.attributes ||
!Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
) {
continue;
}

const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.start));
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.end));

Expand All @@ -553,66 +541,165 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
ts.isIdentifier
);

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()) {
if (!identifier) {
continue;
}

const nonNullable = type.getNonNullableType();
const isQuickFixTargetTargetStore =
identifier?.escapedText.toString().startsWith('$') && diagnostic.code === 2304;
const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(
document,
diagnostic
);

if (
!(
nonNullable.flags & ts.TypeFlags.Object &&
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
)
) {
continue;
if (isQuickFixTargetTargetStore) {
results.push(
...(await this.getSvelteStoreQuickFixes(
identifier,
lang,
document,
tsDoc,
userPreferences
))
);
}

const signature = typeChecker.getSignaturesOfType(
nonNullable,
ts.SignatureKind.Call
)[0];
if (isQuickFixTargetEventHandler) {
results.push(
...this.getEventHandlerQuickFixes(
identifier,
tsDoc,
typeChecker,
quote,
formatCodeBasis
)
);
}
}

const parameters = signature.parameters.map((p) => {
const declaration = p.valueDeclaration ?? p.declarations?.[0];
const typeString = declaration
? typeChecker.typeToString(
typeChecker.getTypeOfSymbolAtLocation(p, declaration)
)
: '';
return results;
}

return { name: p.name, typeString };
});
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
} {`,
formatCodeBasis.indent +
`throw new Error(${quote}Function not implemented.${quote})` +
formatCodeBasis.semi,
'}'
]
.map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
.join('');

result.push({
private async getSvelteStoreQuickFixes(
identifier: ts.Identifier,
lang: ts.LanguageService,
document: Document,
tsDoc: DocumentSnapshot,
userPreferences: ts.UserPreferences
): Promise<ts.CodeFixAction[]> {
const storeIdentifier = identifier.escapedText.toString().substring(1);
const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(
document,
tsDoc.scriptKind
);
const completion = lang.getCompletionsAtPosition(
tsDoc.filePath,
0,
userPreferences,
formatCodeSettings
);

if (!completion) {
return [];
}

const toFix = (c: ts.CompletionEntry) =>
lang
.getCompletionEntryDetails(
tsDoc.filePath,
0,
c.name,
formatCodeSettings,
c.source,
userPreferences,
c.data
)
?.codeActions?.map((a) => ({
...a,
changes: a.changes.map((change) => {
return {
...change,
textChanges: change.textChanges.map((textChange) => {
// For some reason, TS sometimes adds the `type` modifier. Remove it.
return {
...textChange,
newText: textChange.newText.replace(' type ', ' ')
};
})
};
}),
fixName: 'import'
})) ?? [];

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,
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 [];
}

const nonNullable = type.getNonNullableType();

if (
!(
nonNullable.flags & ts.TypeFlags.Object &&
(nonNullable as ts.ObjectType).objectFlags & ts.ObjectFlags.Anonymous
)
) {
return [];
}

const signature = typeChecker.getSignaturesOfType(nonNullable, ts.SignatureKind.Call)[0];

const parameters = signature.parameters.map((p) => {
const declaration = p.valueDeclaration ?? p.declarations?.[0];
const typeString = declaration
? typeChecker.typeToString(typeChecker.getTypeOfSymbolAtLocation(p, declaration))
: '';

return { name: p.name, typeString };
});

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} {`,
formatCodeBasis.indent +
`throw new Error(${quote}Function not implemented.${quote})` +
formatCodeBasis.semi,
'}'
]
.map((line) => formatCodeBasis.baseIndent + line + formatCodeBasis.newLine)
.join('');

return [
{
description: `Add missing function declaration '${identifier.text}'`,
fixName: 'fixMissingFunctionDeclaration',
changes: [
Expand All @@ -626,10 +713,20 @@ export class CodeActionsProviderImpl implements CodeActionsProvider {
]
}
]
});
}
];
}

private isQuickFixForEventHandler(document: Document, diagnostic: Diagnostic) {
const htmlNode = document.html.findNodeAt(document.offsetAt(diagnostic.range.start));
if (
!htmlNode.attributes ||
!Object.keys(htmlNode.attributes).some((attr) => attr.startsWith('on:'))
) {
return false;
}

return result;
return true;
}

private async getApplicableRefactors(
Expand Down

0 comments on commit 5ef7acf

Please sign in to comment.