svelte-language-server
Version:
A language server for Svelte
984 lines • 52 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeActionsProviderImpl = exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND = exports.ADD_MISSING_IMPORTS_CODE_ACTION_KIND = exports.SORT_IMPORT_CODE_ACTION_KIND = void 0;
const svelte2tsx_1 = require("svelte2tsx");
const typescript_1 = __importStar(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';
exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND = 'source.removeUnusedImports';
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, typescript_1.OrganizeImportsMode.SortAndCombine);
}
if (context.only?.[0] === exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND) {
return await this.organizeImports(document, cancellationToken, typescript_1.OrganizeImportsMode.RemoveUnused);
}
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, typescript_1.OrganizeImportsMode.SortAndCombine)),
...(await this.organizeImports(document, cancellationToken, typescript_1.OrganizeImportsMode.RemoveUnused)),
...(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, mode = typescript_1.OrganizeImportsMode.All) {
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',
mode
}, {
...(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));
}));
for (const change of documentChanges) {
this.checkIndentLeftover(change, document);
}
let kind;
let title;
switch (mode) {
case typescript_1.OrganizeImportsMode.SortAndCombine:
kind = exports.SORT_IMPORT_CODE_ACTION_KIND;
title = 'Sort Imports';
break;
case typescript_1.OrganizeImportsMode.RemoveUnused:
kind = exports.REMOVE_UNUSED_IMPORTS_CODE_ACTION_KIND;
title = 'Remove Unused Imports';
break;
default:
kind = vscode_languageserver_1.CodeActionKind.SourceOrganizeImports;
title = 'Organize Imports';
}
return [vscode_languageserver_1.CodeAction.create(title, { documentChanges }, kind)];
}
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) {
if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
return 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 ||
((0, utils_2.isInScript)(range.start, snapshot) && !(0, utils_2.isInScript)(range.end, snapshot))) {
edit.span.length -= 1;
range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, edit.span));
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;
}
checkIndentLeftover(change, document) {
if (!change.edits.length) {
return;
}
const orderedByStart = change.edits.sort((a, b) => {
if (a.range.start.line !== b.range.start.line) {
return a.range.start.line - b.range.start.line;
}
return a.range.start.character - b.range.start.character;
});
let current;
let groups = [];
for (let i = 0; i < orderedByStart.length; i++) {
const edit = orderedByStart[i];
if (!current) {
current = { range: (0, utils_2.cloneRange)(edit.range), newText: edit.newText };
continue;
}
if ((0, utils_1.isPositionEqual)(current.range.end, edit.range.start)) {
current.range.end = edit.range.end;
current.newText += edit.newText;
}
else {
groups.push(current);
current = { range: (0, utils_2.cloneRange)(edit.range), newText: edit.newText };
}
}
if (current) {
groups.push(current);
}
for (const edit of groups) {
if (edit.newText) {
continue;
}
const range = edit.range;
const lineContentBeforeRemove = document.getText({
start: { line: range.start.line, character: 0 },
end: range.start
});
const onlyIndentLeft = !lineContentBeforeRemove.trim();
if (!onlyIndentLeft) {
continue;
}
const lineContentAfterRemove = document.getText({
start: range.end,
end: { line: range.end.line, character: Number.MAX_VALUE }
});
const emptyAfterRemove = !lineContentAfterRemove.trim() || lineContentAfterRemove.startsWith('</script>');
if (emptyAfterRemove) {
change.edits.push({
range: { start: { line: range.start.line, character: 0 }, end: range.start },
newText: ''
});
}
}
}
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, cannotFindNameDiagnostic, tsDoc, 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);
const addLangCodeAction = this.getAddLangTSCodeAction(document, context, tsDoc, formatCodeBasis);
// filter out empty code action
const result = codeActionsNotFilteredOut
.map(({ codeAction }) => codeAction)
.concat(fixAllActions);
return addLangCodeAction ? [addLangCodeAction].concat(result) : result;
}
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, cannotFindNameDiagnostics, tsDoc, userPreferences, formatCodeSettings) {
const program = lang.getProgram();
const sourceFile = program?.getSourceFile(tsDoc.filePath);
if (!program || !sourceFile) {
return [];
}
const results = [];
const getGlobalCompletion = (0, utils_1.memoize)(() => lang.getCompletionsAtPosition(tsDoc.filePath, 0, userPreferences, formatCodeSettings));
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 (!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;
}
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));
}
getAddLangTSCodeAction(document, context, tsDoc, formatCodeBasis) {
if (tsDoc.scriptKind !== typescript_1.default.ScriptKind.JS) {
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;
}
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;
}
return vscode_languageserver_1.CodeAction.create('Add <script lang="ts"> tag', {
documentChanges: [
{
textDocument: vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.uri, null),
edits: [
{
range: vscode_languageserver_1.Range.create(vscode_languageserver_1.Position.create(0, 0), vscode_languageserver_1.Position.create(0, 0)),
newText: '<script lang="ts"></script>' + formatCodeBasis.newLine
}
]
}
]
}, vscode_languageserver_1.CodeActionKind.QuickFix);
}
const edits = [document.scriptInfo, document.moduleScriptInfo]
.map((info) => {
if (!info) {
return;
}
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;
}
return {
range: vscode_languageserver_1.Range.create(startTagNameEnd, startTagNameEnd),
newText: ' lang="ts"'
};
})
.filter(utils_1.isNotNullOrUndefined);
if (edits.length) {
return vscode_languageserver_1.CodeAction.create('Add lang="ts" to <script> tag', {
documentChanges: [
{
textDocument: vscode_languageserver_1.OptionalVersionedTextDocumentIdentifier.create(document.uri, null),
edits
}
]
}, vscode_languageserver_1.CodeActionKind.QuickFix);
}
}
hasLangTsScriptTag(node) {
if (node.tag === 'script' &&
(node.attributes?.lang === '"ts"' || node.attributes?.lang === "'ts'") &&
node.parent) {
return true;
}
for (const element of node.children) {
if (this.hasLangTsScriptTag(element)) {
return true;
}
}
return false;
}
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