svelte-language-server
Version:
A language server for Svelte
921 lines • 48 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeActionsProviderImpl = exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND = exports.SORT_IMPORT_CODE_ACTION_KIND = void 0;
const svelte2tsx_1 = require("svelte2tsx");
const typescript_1 = __importDefault(require("typescript"));
const vscode_languageserver_1 = require("vscode-languageserver");
const documents_1 = require("../../../lib/documents");
const utils_1 = require("../../../utils");
const DocumentSnapshot_1 = require("../DocumentSnapshot");
const utils_2 = require("../utils");
const DiagnosticsProvider_1 = require("./DiagnosticsProvider");
const utils_3 = require("./utils");
/**
* TODO change this to protocol constant if it's part of the protocol
*/
exports.SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports';
exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND = 'source.addMissingImports';
const FIX_IMPORT_FIX_NAME = 'import';
const FIX_IMPORT_FIX_ID = 'fixMissingImport';
const FIX_IMPORT_FIX_DESCRIPTION = 'Add all missing imports';
const nonIdentifierRegex = /[\`\~\!\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>/\?\s]/;
class CodeActionsProviderImpl {
constructor(lsAndTsDocResolver, completionProvider, configManager) {
this.lsAndTsDocResolver = lsAndTsDocResolver;
this.completionProvider = completionProvider;
this.configManager = configManager;
}
async getCodeActions(document, range, context, cancellationToken) {
if (context.only?.[0] === vscode_languageserver_1.CodeActionKind.SourceOrganizeImports) {
return await this.organizeImports(document, cancellationToken);
}
if (context.only?.[0] === exports.SORT_IMPORT_CODE_ACTION_KIND) {
return await this.organizeImports(document, cancellationToken,
/**skipDestructiveCodeActions */ true);
}
if (context.only?.[0] === exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND) {
return await this.addMissingImports(document, cancellationToken);
}
// for source action command (all source.xxx)
// vscode would show different source code action kinds to choose from
if (context.only?.[0] === vscode_languageserver_1.CodeActionKind.Source) {
return [
...(await this.organizeImports(document, cancellationToken)),
...(await this.organizeImports(document, cancellationToken,
/**skipDestructiveCodeActions */ true)),
...(await this.addMissingImports(document, cancellationToken))
];
}
if (context.diagnostics.length &&
(!context.only || context.only.includes(vscode_languageserver_1.CodeActionKind.QuickFix))) {
return await this.applyQuickfix(document, range, context, cancellationToken);
}
if (!context.only || context.only.includes(vscode_languageserver_1.CodeActionKind.Refactor)) {
return await this.getApplicableRefactors(document, range, cancellationToken);
}
return [];
}
async resolveCodeAction(document, codeAction, cancellationToken) {
if (!this.isQuickFixAllResolveInfo(codeAction.data)) {
return codeAction;
}
const { lang, tsDoc, userPreferences, lsContainer } = await this.lsAndTsDocResolver.getLSAndTSDoc(document);
if (cancellationToken?.isCancellationRequested) {
return codeAction;
}
const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
const formatCodeBasis = (0, utils_3.getFormatCodeBasis)(formatCodeSettings);
const getDiagnostics = (0, utils_1.memoize)(() => lang.getSemanticDiagnostics(tsDoc.filePath).map((dia) => ({
range: (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, dia)),
message: '',
code: dia.code
})));
const isImportFix = codeAction.data.fixName === FIX_IMPORT_FIX_NAME;
const virtualDocInfo = isImportFix
? this.createVirtualDocumentForCombinedImportCodeFix(document, getDiagnostics(), tsDoc, lsContainer, lang)
: undefined;
const fix = lang.getCombinedCodeFix({
type: 'file',
fileName: (virtualDocInfo?.virtualDoc ?? document).getFilePath()
}, codeAction.data.fixId, formatCodeSettings, userPreferences);
if (virtualDocInfo) {
const getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(typescript_1.default.sys.useCaseSensitiveFileNames);
const virtualDocPath = getCanonicalFileName((0, utils_1.normalizePath)(virtualDocInfo.virtualDoc.getFilePath()));
for (const change of fix.changes) {
if (getCanonicalFileName((0, utils_1.normalizePath)(change.fileName)) === virtualDocPath) {
change.fileName = tsDoc.filePath;
this.removeDuplicatedComponentImport(virtualDocInfo.insertedNames, change);
}
}
await this.lsAndTsDocResolver.deleteSnapshot(virtualDocPath);
}
const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver, lsContainer);
const fixActions = [
{
fixName: codeAction.data.fixName,
changes: Array.from(fix.changes),
description: ''
}
];
const documentChangesPromises = fixActions.map((fix) => this.convertAndFixCodeFixAction({
document,
fix,
formatCodeBasis,
formatCodeSettings,
getDiagnostics,
snapshots,
skipAddScriptTag: true
}));
const documentChanges = (await Promise.all(documentChangesPromises)).flat();
if (cancellationToken?.isCancellationRequested) {
return codeAction;
}
if (isImportFix) {
this.fixCombinedImportQuickFix(documentChanges, document, formatCodeBasis);
}
codeAction.edit = {
documentChanges
};
return codeAction;
}
/**
* Do not use this in regular code action
* This'll cause TypeScript to rebuild and invalidate caches every time. It'll be slow
*/
createVirtualDocumentForCombinedImportCodeFix(document, diagnostics, tsDoc, lsContainer, lang) {
const virtualUri = document.uri + '.__virtual__.svelte';
const names = new Set();
const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath);
if (!sourceFile) {
return undefined;
}
for (const diagnostic of diagnostics) {
if (diagnostic.range.start.line < 0 ||
diagnostic.range.end.line < 0 ||
(diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME &&
diagnostic.code !== DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y)) {
continue;
}
const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
const name = identifier?.text;
if (!name || names.has(name)) {
continue;
}
if (name.startsWith('$')) {
names.add(name.slice(1));
}
else if (!(0, utils_2.isInScript)(diagnostic.range.start, document)) {
if (this.isComponentStartTag(identifier)) {
names.add((0, utils_2.toGeneratedSvelteComponentName)(name));
}
}
}
if (!names.size) {
return undefined;
}
const inserts = Array.from(names.values())
.map((name) => name + ';')
.join('');
// assumption: imports are always at the top of the script tag
// so these appends won't change the position of the edits
const text = document.getText();
const newText = document.scriptInfo
? text.slice(0, document.scriptInfo.end) + inserts + text.slice(document.scriptInfo.end)
: `${document.getText()}<script>${inserts}</script>`;
const virtualDoc = new documents_1.Document(virtualUri, newText);
virtualDoc.openedByClient = true;
// let typescript know about the virtual document
lsContainer.openVirtualDocument(virtualDoc);
lsContainer.getService();
return {
virtualDoc,
insertedNames: names
};
}
/**
* Remove component default import if there is a named import with the same name
* Usually happens with reexport or inheritance of component library
*/
removeDuplicatedComponentImport(insertedNames, change) {
for (const name of insertedNames) {
const unSuffixedNames = (0, utils_2.changeSvelteComponentName)(name);
const matchRegex = unSuffixedNames != name && this.toImportMemberRegex(unSuffixedNames);
if (!matchRegex ||
!change.textChanges.some((textChange) => textChange.newText.match(matchRegex))) {
continue;
}
const importRegex = new RegExp(`\\s+import ${name} from ('|")(.*)('|");?\r?\n?`);
change.textChanges = change.textChanges
.map((textChange) => ({
...textChange,
newText: textChange.newText.replace(importRegex, (match) => {
if (match.split('\n').length > 2) {
return '\n';
}
else {
return '';
}
})
}))
// in case there are replacements
.filter((change) => change.span.length || change.newText);
}
}
fixCombinedImportQuickFix(documentChanges, document, formatCodeBasis) {
if (!documentChanges.length || document.scriptInfo || document.moduleScriptInfo) {
return;
}
const editForThisFile = documentChanges.find((change) => change.textDocument.uri === document.uri);
if (editForThisFile?.edits.length) {
const [first] = editForThisFile.edits;
first.newText =
(0, utils_3.getNewScriptStartTag)(this.configManager.getConfig(), formatCodeBasis.newLine) +
formatCodeBasis.baseIndent +
first.newText.trimStart();
const last = editForThisFile.edits[editForThisFile.edits.length - 1];
last.newText = last.newText + '</script>' + formatCodeBasis.newLine;
}
}
toImportMemberRegex(name) {
return new RegExp(`${name}($| |,)`);
}
isQuickFixAllResolveInfo(data) {
const asserted = data;
return asserted?.fixId != undefined && typeof asserted.fixName === 'string';
}
async organizeImports(document, cancellationToken, skipDestructiveCodeActions = false) {
if (!document.scriptInfo && !document.moduleScriptInfo) {
return [];
}
const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
if (cancellationToken?.isCancellationRequested || tsDoc.parserError) {
// If there's a parser error, we fall back to only the script contents,
// so organize imports likely throws out a lot of seemingly unused imports
// because they are only used in the template. Therefore do nothing in this case.
return [];
}
const changes = lang.organizeImports({
fileName: tsDoc.filePath,
type: 'file',
skipDestructiveCodeActions
}, {
...(await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind)),
// handle it on our own
baseIndentSize: undefined
}, userPreferences);
const documentChanges = await Promise.all(changes.map(async (change) => {
// Organize Imports will only affect the current file, so no need to check the file path
return vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.url, null), change.textChanges
.map((edit) => {
const range = this.checkRemoveImportCodeActionRange(edit, tsDoc, (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, edit.span)));
edit.newText = (0, utils_1.removeLineWithString)(edit.newText, 'SvelteComponentTyped as __SvelteComponentTyped__');
return this.fixIndentationOfImports(vscode_languageserver_1.TextEdit.replace(range, edit.newText), document);
})
.filter((edit) =>
// The __SvelteComponentTyped__ import is added by us and will have a negative mapped line
edit.range.start.line !== -1));
}));
return [
vscode_languageserver_1.CodeAction.create(skipDestructiveCodeActions ? 'Sort Imports' : 'Organize Imports', { documentChanges }, skipDestructiveCodeActions
? exports.SORT_IMPORT_CODE_ACTION_KIND
: vscode_languageserver_1.CodeActionKind.SourceOrganizeImports)
];
}
fixIndentationOfImports(edit, document) {
// "Organize Imports" will have edits that delete a group of imports by return empty edits
// and one edit which contains all the organized imports of the group. Fix indentation
// of that one by prepending all lines with the indentation of the first line.
const { newText, range } = edit;
if (!newText || range.start.character === 0) {
return edit;
}
const line = (0, documents_1.getLineAtPosition)(range.start, document.getText());
const leadingChars = line.substring(0, range.start.character);
if (leadingChars.trim() !== '') {
return edit;
}
const fixedNewText = (0, utils_1.modifyLines)(edit.newText, (line, idx) => idx === 0 || !line ? line : leadingChars + line);
if (range.end.character > 0) {
const endLine = (0, documents_1.getLineAtPosition)(range.end, document.getText());
const isIndent = !endLine.substring(0, range.end.character).trim();
if (isIndent) {
const trimmedEndLine = endLine.trim();
// imports that would be removed by the next delete edit
if (trimmedEndLine && !trimmedEndLine.startsWith('import')) {
range.end.character = 0;
}
}
}
return vscode_languageserver_1.TextEdit.replace(range, fixedNewText);
}
checkRemoveImportCodeActionRange(edit, snapshot, range) {
// Handle svelte2tsx wrong import mapping:
// The character after the last import maps to the start of the script
// TODO find a way to fix this in svelte2tsx and then remove this
if ((range.end.line === 0 && range.end.character === 1) ||
range.end.line < range.start.line) {
edit.span.length -= 1;
range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, edit.span));
if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
range.end.character += 1;
return range;
}
const line = (0, documents_1.getLineAtPosition)(range.end, snapshot.getOriginalText());
// remove-import code action will removes the
// line break generated by svelte2tsx,
// but when there's no line break in the source
// move back to next character would remove the next character
if ([';', '"', "'"].includes(line[range.end.character])) {
range.end.character += 1;
}
if ((0, documents_1.isAtEndOfLine)(line, range.end.character)) {
range.end.line += 1;
range.end.character = 0;
}
}
return range;
}
async applyQuickfix(document, range, context, cancellationToken) {
const { lang, tsDoc, userPreferences, lsContainer } = await this.getLSAndTSDoc(document);
if (cancellationToken?.isCancellationRequested) {
return [];
}
const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start));
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end));
const errorCodes = context.diagnostics.map((diag) => Number(diag.code));
const cannotFindNameDiagnostic = context.diagnostics.filter((diagnostic) => diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME ||
diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y);
const formatCodeSettings = await this.configManager.getFormatCodeSettingsForFile(document, tsDoc.scriptKind);
const formatCodeBasis = (0, utils_3.getFormatCodeBasis)(formatCodeSettings);
let codeFixes = cannotFindNameDiagnostic.length
? this.getComponentImportQuickFix(document, lang, tsDoc, userPreferences, cannotFindNameDiagnostic, formatCodeSettings)
: undefined;
// either-or situation when it's not a "did you mean" fix
if (codeFixes === undefined ||
errorCodes.includes(DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y)) {
codeFixes ??= [];
codeFixes = codeFixes.concat(...lang.getCodeFixesAtPosition(tsDoc.filePath, start, end, errorCodes, formatCodeSettings, userPreferences), ...this.getSvelteQuickFixes(lang, document, cannotFindNameDiagnostic, tsDoc, formatCodeBasis, userPreferences, formatCodeSettings));
}
const snapshots = new utils_3.SnapshotMap(this.lsAndTsDocResolver, lsContainer);
snapshots.set(tsDoc.filePath, tsDoc);
const codeActionsPromises = codeFixes.map(async (fix) => {
const documentChanges = await this.convertAndFixCodeFixAction({
fix,
snapshots,
document,
formatCodeSettings,
formatCodeBasis,
getDiagnostics: () => context.diagnostics
});
const codeAction = vscode_languageserver_1.CodeAction.create(fix.description, {
documentChanges
}, vscode_languageserver_1.CodeActionKind.QuickFix);
return {
fix,
codeAction
};
});
const identifier = {
uri: document.uri
};
const codeActions = await Promise.all(codeActionsPromises);
if (cancellationToken?.isCancellationRequested) {
return [];
}
const codeActionsNotFilteredOut = codeActions.filter(({ codeAction }) => codeAction.edit?.documentChanges?.every((change) => change.edits.length > 0));
const fixAllActions = this.getFixAllActions(codeActionsNotFilteredOut.map(({ fix }) => fix), identifier, tsDoc.filePath, lang);
// filter out empty code action
return codeActionsNotFilteredOut.map(({ codeAction }) => codeAction).concat(fixAllActions);
}
async convertAndFixCodeFixAction({ fix, snapshots, document, formatCodeSettings, formatCodeBasis, getDiagnostics, skipAddScriptTag }) {
const documentChangesPromises = fix.changes.map(async (change) => {
const snapshot = await snapshots.retrieve(change.fileName);
return vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create((0, utils_1.pathToUrl)(change.fileName), null), change.textChanges
.map((edit) => {
if (fix.fixName === FIX_IMPORT_FIX_NAME &&
snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot) {
const namePosition = 'position' in fix ? fix.position : undefined;
const startPos = namePosition ??
this.findDiagnosticForImportFix(document, edit, getDiagnostics())
?.range?.start ??
vscode_languageserver_1.Position.create(0, 0);
return this.completionProvider.codeActionChangeToTextEdit(document, snapshot, edit, true, startPos, formatCodeBasis.newLine, undefined, skipAddScriptTag);
}
if ((0, utils_3.isTextSpanInGeneratedCode)(snapshot.getFullText(), edit.span)) {
return undefined;
}
let originalRange = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, edit.span));
if (fix.fixName === 'unusedIdentifier') {
originalRange = this.checkRemoveImportCodeActionRange(edit, snapshot, originalRange);
}
if (fix.fixName === 'fixAwaitInSyncFunction' && document.scriptInfo) {
const scriptStartTagStart = document.scriptInfo.container.start;
const scriptStartTagEnd = document.scriptInfo.start;
const withinStartTag = document.offsetAt(originalRange.start) < scriptStartTagEnd &&
document.offsetAt(originalRange.end) > scriptStartTagStart;
if (withinStartTag) {
return undefined;
}
}
if (fix.fixName === 'fixMissingFunctionDeclaration') {
const position = 'position' in fix ? fix.position : undefined;
const checkRange = position
? vscode_languageserver_1.Range.create(position, position)
: this.findDiagnosticForQuickFix(document, DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME, getDiagnostics(), (possiblyIdentifier) => {
return edit.newText.includes('function ' + possiblyIdentifier + '(');
})?.range;
originalRange = this.checkEndOfFileCodeInsert(originalRange, checkRange, document);
// ts doesn't add base indent to the first line
if (formatCodeSettings.baseIndentSize) {
const emptyLine = formatCodeBasis.newLine.repeat(2);
edit.newText =
emptyLine +
formatCodeBasis.baseIndent +
edit.newText.trimLeft();
}
}
if (fix.fixName === 'disableJsDiagnostics') {
if (edit.newText.includes('ts-nocheck')) {
return this.checkTsNoCheckCodeInsert(document, edit);
}
return this.checkDisableJsDiagnosticsCodeInsert(originalRange, document, edit);
}
if (fix.fixName === 'inferFromUsage') {
originalRange = this.checkAddJsDocCodeActionRange(snapshot, originalRange, document);
}
if (fix.fixName === 'fixConvertConstToLet') {
const offset = document.offsetAt(originalRange.start);
const constOffset = document.getText().indexOf('const', offset);
if (constOffset < 0) {
return undefined;
}
const beforeConst = document.getText().slice(0, constOffset);
if (beforeConst[beforeConst.length - 1] === '@' &&
beforeConst
.slice(0, beforeConst.length - 1)
.trimEnd()
.endsWith('{')) {
return undefined;
}
}
if (originalRange.start.line < 0 || originalRange.end.line < 0) {
return undefined;
}
return vscode_languageserver_1.TextEdit.replace(originalRange, edit.newText);
})
.filter(utils_1.isNotNullOrUndefined));
});
const documentChanges = await Promise.all(documentChangesPromises);
return documentChanges;
}
findDiagnosticForImportFix(document, edit, diagnostics) {
return this.findDiagnosticForQuickFix(document, DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME, diagnostics, (possibleIdentifier) => !nonIdentifierRegex.test(possibleIdentifier) &&
this.toImportMemberRegex(possibleIdentifier).test(edit.newText));
}
findDiagnosticForQuickFix(document, targetCode, diagnostics, match) {
const diagnostic = diagnostics.find((diagnostic) => {
if (diagnostic.code !== targetCode) {
return false;
}
const possibleIdentifier = document.getText(diagnostic.range);
if (possibleIdentifier) {
return match(possibleIdentifier);
}
return false;
});
return diagnostic;
}
getFixAllActions(codeFixes, identifier, fileName, lang) {
const checkedFixIds = new Set();
const fixAll = [];
for (const codeFix of codeFixes) {
if (!codeFix.fixId || !codeFix.fixAllDescription || checkedFixIds.has(codeFix.fixId)) {
continue;
}
// we have custom fix for import
// check it again if fix-all might be necessary
if (codeFix.fixName === FIX_IMPORT_FIX_NAME) {
const allCannotFindNameDiagnostics = lang
.getSemanticDiagnostics(fileName)
.filter((diagnostic) => diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME ||
diagnostic.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y);
if (allCannotFindNameDiagnostics.length < 2) {
checkedFixIds.add(codeFix.fixId);
continue;
}
}
const codeAction = vscode_languageserver_1.CodeAction.create(codeFix.fixAllDescription, vscode_languageserver_1.CodeActionKind.QuickFix);
const data = {
...identifier,
fixName: codeFix.fixName,
fixId: codeFix.fixId
};
codeAction.data = data;
checkedFixIds.add(codeFix.fixId);
fixAll.push(codeAction);
}
return fixAll;
}
/**
* import quick fix requires the symbol name to be the same as where it's defined.
* But we have suffix on component default export to prevent conflict with
* a local variable. So we use auto-import completion as a workaround here.
*/
getComponentImportQuickFix(document, lang, tsDoc, userPreferences, diagnostics, formatCodeSetting) {
const sourceFile = lang.getProgram()?.getSourceFile(tsDoc.filePath);
if (!sourceFile) {
return;
}
const nameToPosition = new Map();
for (const diagnostic of diagnostics) {
if ((0, utils_2.isInScript)(diagnostic.range.start, document)) {
continue;
}
const possibleIdentifier = document.getText(diagnostic.range);
if (!possibleIdentifier ||
!(0, utils_1.possiblyComponent)(possibleIdentifier) ||
nameToPosition.has(possibleIdentifier)) {
continue;
}
const node = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
if (!node || !this.isComponentStartTag(node)) {
return;
}
const tagNameEnd = node.getEnd();
const name = node.getText();
if ((0, utils_1.possiblyComponent)(name)) {
nameToPosition.set(name, tagNameEnd);
}
}
if (!nameToPosition.size) {
return;
}
const result = [];
for (const [name, position] of nameToPosition) {
const errorPreventingUserPreferences = this.completionProvider.fixUserPreferencesForSvelteComponentImport(userPreferences);
const resolvedCompletion = (c) => lang.getCompletionEntryDetails(tsDoc.filePath, position, c.name, formatCodeSetting, c.source, errorPreventingUserPreferences, c.data);
const toFix = (c) => c.codeActions?.map((a) => ({
...a,
description: (0, utils_2.changeSvelteComponentName)(a.description),
fixName: FIX_IMPORT_FIX_NAME,
fixId: FIX_IMPORT_FIX_ID,
fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION,
position: originalPosition
})) ?? [];
const completion = lang.getCompletionsAtPosition(tsDoc.filePath, position, userPreferences, formatCodeSetting);
const entries = completion?.entries
.filter((c) => c.name === name || c.name === (0, utils_2.toGeneratedSvelteComponentName)(name))
.map(resolvedCompletion)
.sort((a, b) => this.numberOfDirectorySeparators(typescript_1.default.displayPartsToString(a?.sourceDisplay ?? [])) -
this.numberOfDirectorySeparators(typescript_1.default.displayPartsToString(b?.sourceDisplay ?? [])))
.filter(utils_1.isNotNullOrUndefined);
if (!entries?.length) {
continue;
}
const originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(position));
const resultForName = entries.flatMap(toFix);
result.push(...resultForName);
}
return result;
}
isComponentStartTag(node) {
return (typescript_1.default.isCallExpression(node.parent) &&
typescript_1.default.isIdentifier(node.parent.expression) &&
node.parent.expression.text === '__sveltets_2_ensureComponent' &&
typescript_1.default.isIdentifier(node));
}
numberOfDirectorySeparators(path) {
return path.split('/').length - 1;
}
getSvelteQuickFixes(lang, document, cannotFindNameDiagnostics, tsDoc, formatCodeBasis, userPreferences, formatCodeSettings) {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(tsDoc.filePath);
if (!program || !sourceFile) {
return [];
}
const typeChecker = program.getTypeChecker();
const results = [];
const quote = (0, utils_3.getQuotePreference)(sourceFile, userPreferences);
const getGlobalCompletion = (0, utils_1.memoize)(() => lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings));
const [tsMajorStr] = typescript_1.default.version.split('.');
const tsSupportHandlerQuickFix = parseInt(tsMajorStr) >= 5;
for (const diagnostic of cannotFindNameDiagnostics) {
const identifier = this.findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile);
if (!identifier) {
continue;
}
const isQuickFixTargetTargetStore = identifier?.escapedText.toString().startsWith('$');
const fixes = [];
if (isQuickFixTargetTargetStore) {
fixes.push(...this.getSvelteStoreQuickFixes(identifier, lang, tsDoc, userPreferences, formatCodeSettings, getGlobalCompletion));
}
if (!tsSupportHandlerQuickFix) {
const isQuickFixTargetEventHandler = this.isQuickFixForEventHandler(document, diagnostic);
if (isQuickFixTargetEventHandler) {
fixes.push(...this.getEventHandlerQuickFixes(identifier, tsDoc, typeChecker, quote, formatCodeBasis));
}
}
if (!fixes.length) {
continue;
}
const originalPosition = tsDoc.getOriginalPosition(tsDoc.positionAt(identifier.pos));
results.push(...fixes.map((fix) => ({
name: identifier.getText(),
position: originalPosition,
...fix
})));
}
return results;
}
findIdentifierForDiagnostic(tsDoc, diagnostic, sourceFile) {
const start = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.start));
const end = tsDoc.offsetAt(tsDoc.getGeneratedPosition(diagnostic.range.end));
const identifier = (0, utils_3.findClosestContainingNode)(sourceFile, { start, length: end - start }, typescript_1.default.isIdentifier);
return identifier;
}
// TODO: Remove this in late 2023
// when most users have upgraded to TS 5.0+
getSvelteStoreQuickFixes(identifier, lang, tsDoc, userPreferences, formatCodeSettings, getCompletions) {
const storeIdentifier = identifier.escapedText.toString().substring(1);
const completion = getCompletions();
if (!completion) {
return [];
}
const toFix = (c) => 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: FIX_IMPORT_FIX_NAME,
fixId: FIX_IMPORT_FIX_ID,
fixAllDescription: FIX_IMPORT_FIX_DESCRIPTION
})) ?? [];
return (0, utils_1.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.
*/
getEventHandlerQuickFixes(identifier, tsDoc, typeChecker, quote, formatCodeBasis) {
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 & typescript_1.default.TypeFlags.Object &&
nonNullable.objectFlags & typescript_1.default.ObjectFlags.Anonymous)) {
return [];
}
const signature = typeChecker.getSignaturesOfType(nonNullable, typescript_1.default.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 === typescript_1.default.ScriptKind.JS || tsDoc.scriptKind === typescript_1.default.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 [
{
description: `Add missing function declaration '${identifier.text}'`,
fixName: 'fixMissingFunctionDeclaration',
changes: [
{
fileName: tsDoc.filePath,
textChanges: [
{
newText,
span: { start: 0, length: 0 }
}
]
}
]
}
];
}
isQuickFixForEventHandler(document, 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 true;
}
async getApplicableRefactors(document, range, cancellationToken) {
if (!(0, documents_1.isRangeInTag)(range, document.scriptInfo) &&
!(0, documents_1.isRangeInTag)(range, document.moduleScriptInfo)) {
return [];
}
// Don't allow refactorings when there is likely a store subscription.
// Reason: Extracting that would lead to svelte2tsx' transformed store representation
// showing up, which will confuse the user. In the long run, we maybe have to
// setup a separate ts language service which only knows of the original script.
const textInRange = document
.getText()
.substring(document.offsetAt(range.start), document.offsetAt(range.end));
if (textInRange.includes('$')) {
return [];
}
const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
if (cancellationToken?.isCancellationRequested) {
return [];
}
const textRange = {
pos: tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.start)),
end: tsDoc.offsetAt(tsDoc.getGeneratedPosition(range.end))
};
const applicableRefactors = lang.getApplicableRefactors(document.getFilePath() || '', textRange, userPreferences);
return (this.applicableRefactorsToCodeActions(applicableRefactors, document, range, textRange)
// Only allow refactorings from which we know they work
.filter((refactor) => refactor.command?.command.includes('function_scope') ||
refactor.command?.command.includes('constant_scope') ||
refactor.command?.command === 'Infer function return type')
// The language server also proposes extraction into const/function in module scope,
// which is outside of the render function, which is svelte2tsx-specific and unmapped,
// so it would both not work and confuse the user ("What is this render? Never declared that").
// So filter out the module scope proposal and rename the render-title
.filter((refactor) => !refactor.title.includes('module scope'))
.map((refactor) => ({
...refactor,
title: refactor.title
.replace(`Extract to inner function in function '${svelte2tsx_1.internalHelpers.renderName}'`, 'Extract to function')
.replace(`Extract to constant in function '${svelte2tsx_1.internalHelpers.renderName}'`, 'Extract to constant')
})));
}
applicableRefactorsToCodeActions(applicableRefactors, document, originalRange, textRange) {
return (0, utils_1.flatten)(applicableRefactors.map((applicableRefactor) => {
if (applicableRefactor.inlineable === false) {
return [
vscode_languageserver_1.CodeAction.create(applicableRefactor.description, {
title: applicableRefactor.description,
command: applicableRefactor.name,
arguments: [
document.uri,
{
type: 'refactor',
textRange,
originalRange,
refactorName: 'Extract Symbol'
}
]
})
];
}
return applicableRefactor.actions.map((action) => {
return vscode_languageserver_1.CodeAction.create(action.description, {
title: action.description,
command: action.name,
arguments: [
document.uri,
{
type: 'refactor',
textRange,
originalRange,
refactorName: applicableRefactor.name
}
]
});
});
}));
}
async executeCommand(document, command, args) {
if (!(args?.[1]?.type === 'refactor')) {
return null;
}
const { lang, tsDoc, userPreferences } = await this.getLSAndTSDoc(document);
const path = document.getFilePath() || '';
const { refactorName, originalRange, textRange } = args[1];
const edits = lang.getEditsForRefactor(path, {}, textRange, refactorName, command, userPreferences);
if (!edits || edits.edits.length === 0) {
return null;
}
const documentChanges = edits?.edits.map((edit) => vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.uri, null), edit.textChanges.map((edit) => {
const range = (0, documents_1.mapRangeToOriginal)(tsDoc, (0, utils_2.convertRange)(tsDoc, edit.span));
return vscode_languageserver_1.TextEdit.replace(this.checkEndOfFileCodeInsert(range, originalRange, document), edit.newText);
})));
return { documentChanges };
}
/**
* Some refactorings place the new code at the end of svelte2tsx' render function,
* which is unmapped. In this case, add it to the end of the script tag ourselves.
*/
checkEndOfFileCodeInsert(resultRange, targetRange, document) {
if (resultRange.start.line < 0 || resultRange.end.line < 0) {
if (document.moduleScriptInfo &&
(!targetRange || (0, documents_1.isRangeInTag)(targetRange, document.moduleScriptInfo))) {
return vscode_languageserver_1.Range.create(document.moduleScriptInfo.endPos, document.moduleScriptInfo.endPos);
}
if (document.scriptInfo) {
return vscode_languageserver_1.Range.create(document.scriptInfo.endPos, document.scriptInfo.endPos);
}
}
// don't add script tag here because the code action is calculated
// when the file is treated as js
// but user might want a ts version of the code action
return resultRange;
}
checkTsNoCheckCodeInsert(document, edit) {
const scriptInfo = document.moduleScriptInfo ?? document.scriptInfo;
if (!scriptInfo) {
return undefined;
}
const newText = typescript_1.default.sys.newLine + edit.newText;
return vscode_languageserver_1.TextEdit.insert(scriptInfo.startPos, newText);
}
checkDisableJsDiagnosticsCodeInsert(originalRange, document, edit) {
const inModuleScript = (0, documents_1.isInTag)(originalRange.start, document.moduleScriptInfo);
if (!(0, documents_1.isInTag)(originalRange.start, document.scriptInfo) && !inModuleScript) {
return null;
}
const position = inModuleScript
? originalRange.start
: (this.fixPropsCodeActionRange(originalRange.start, document) ?? originalRange.start);
// fix the length of trailing indent
const linesOfNewText = edit.newText.split('\n');
if (/^[ \t]*$/.test(linesOfNewText[linesOfNewText.length - 1])) {
const line = (0, documents_1.getLineAtPosition)(originalRange.start, document.getText());
const indent = (0, utils_1.getIndent)(line);
linesOfNewText[linesOfNewText.length - 1] = indent;
}
return vscode_languageserver_1.TextEdit.insert(position, linesOfNewText.join('\n'));
}
/**
* svelte2tsx removes export in instance script
*/
fixPropsCodeActionRange(start, document) {
const documentText = document.getText();
const offset = document.offsetAt(start);
const exportKeywordOffset = documentText.lastIndexOf('export', offset);
// export let a;
if (exportKeywordOffset < 0 ||
documentText.slice(exportKeywordOffset + 'export'.length, offset).trim()) {
return;
}
const charBeforeExport = documentText[exportKeywordOffset - 1];
if ((charBeforeExport !== undefined && !charBeforeExport.trim()) ||
charBeforeExport === ';') {
return document.positionAt(exportKeywordOffset);
}
}
checkAddJsDocCodeActionRange(snapshot, originalRange, document) {
if (snapshot.scriptKind !== typescript_1.default.ScriptKind.JS &&
snapshot.scriptKind !== typescript_1.default.ScriptKind.JSX &&
!(0, documents_1.isInTag)(originalRange.start, document.scriptInfo)) {
return originalRange;
}
const position = this.fixPropsCodeActionRange(originalRange.start, document);
if (position) {
return {
start: position,
end: position
};
}
return originalRange;
}
async getLSAndTSDoc(document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
async addMissingImports(document, cancellationToken) {
// Re-introduce LS/TSDoc resolution and diagnostic check
const { lang, tsDoc } = await this.getLSAndTSDoc(document);
if (cancellationToken?.isCancellationRequested) {
return [];
}
// Check if there are any relevant "cannot find name" diagnostics
const diagnostics = lang.getSemanticDiagnostics(tsDoc.filePath);
const hasMissingImports = diagnostics.some((diag) => (diag.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME ||
diag.code === DiagnosticsProvider_1.DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y) &&
// Ensure the diagnostic is not in generated code
!(0, utils_3.isTextSpanInGeneratedCode)(tsDoc.getFullText(), {
start: diag.start ?? 0,
length: diag.length ?? 0
}));
// Only return the action if there are potential imports to add
if (!hasMissingImports) {
return [];
}
// If imports might be needed, create the deferred action
const codeAction = vscode_languageserver_1.CodeAction.create(FIX_IMPORT_FIX_DESCRIPTION, exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND);
const data = {
uri: document.uri,
fixName: FIX_IMPORT_FIX_NAME,
fixId: FIX_IMPORT_FIX_ID
};
codeAction.data = data;
return [codeAction];
}
}
exports.CodeActionsProviderImpl = CodeActionsProviderImpl;
//# sourceMappingURL=CodeActionsProvider.js.map