svelte-language-server
Version:
A language server for Svelte
423 lines • 22.5 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RenameProviderImpl = void 0;
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 typescript_1 = __importDefault(require("typescript"));
const utils_3 = require("./utils");
const svelte_ast_utils_1 = require("../svelte-ast-utils");
const bind = 'bind:';
class RenameProviderImpl {
constructor(lsAndTsDocResolver, configManager) {
this.lsAndTsDocResolver = lsAndTsDocResolver;
this.configManager = configManager;
}
// TODO props written as `export {x as y}` are not supported yet.
async prepareRename(document, position) {
const { lang, tsDoc } = await this.getLSAndTSDoc(document);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
const renameInfo = this.getRenameInfo(lang, tsDoc, document, position, offset);
if (!renameInfo) {
return null;
}
return this.mapRangeToOriginal(tsDoc, renameInfo.triggerSpan);
}
async rename(document, position, newName) {
const { lang, tsDoc, lsContainer } = await this.getLSAndTSDoc(document);
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
const renameInfo = this.getRenameInfo(lang, tsDoc, document, position, offset);
if (!renameInfo) {
return null;
}
const renameLocations = lang.findRenameLocations(tsDoc.filePath, offset, false, false, true);
if (!renameLocations) {
return null;
}
const docs = new utils_3.SnapshotMap(this.lsAndTsDocResolver, lsContainer);
docs.set(tsDoc.filePath, tsDoc);
let convertedRenameLocations = await this.mapAndFilterRenameLocations(renameLocations, docs, renameInfo.isStore ? `$${newName}` : undefined);
convertedRenameLocations.push(...(await this.enhanceRenamesInCaseOf$Store(renameLocations, newName, docs, lang)));
convertedRenameLocations = this.checkShortHandBindingOrSlotLetLocation(lang, convertedRenameLocations, docs, newName);
const additionalRenameForPropRenameInsideComponentWithProp = await this.getAdditionLocationsForRenameOfPropInsideComponentWithProp(document, tsDoc, position, convertedRenameLocations, docs, lang, newName);
const additionalRenamesForPropRenameOutsideComponentWithProp =
// This is an either-or-situation, don't do both
additionalRenameForPropRenameInsideComponentWithProp.length > 0
? []
: await this.getAdditionalLocationsForRenameOfPropInsideOtherComponent(convertedRenameLocations, docs, lang, tsDoc.filePath);
convertedRenameLocations = [
...convertedRenameLocations,
...additionalRenameForPropRenameInsideComponentWithProp,
...additionalRenamesForPropRenameOutsideComponentWithProp
];
return (0, utils_1.unique)(convertedRenameLocations.filter((loc) => loc.range.start.line >= 0 && loc.range.end.line >= 0)).reduce((acc, loc) => {
const uri = (0, utils_1.pathToUrl)(loc.fileName);
if (!acc.changes[uri]) {
acc.changes[uri] = [];
}
acc.changes[uri].push({
newText: (loc.prefixText || '') + (loc.newName || newName) + (loc.suffixText || ''),
range: loc.range
});
return acc;
}, { changes: {} });
}
getRenameInfo(lang, tsDoc, doc, originalPosition, generatedOffset) {
// Don't allow renames in error-state, because then there is no generated svelte2tsx-code
// and rename cannot work
if (tsDoc.parserError) {
return null;
}
const svelteNode = tsDoc.svelteNodeAt(originalPosition);
const renameInfo = lang.getRenameInfo(tsDoc.filePath, generatedOffset, {
allowRenameOfImportPath: false
});
if (!renameInfo.canRename ||
renameInfo.fullDisplayName?.includes('JSX.IntrinsicElements') ||
(renameInfo.kind === typescript_1.default.ScriptElementKind.jsxAttribute &&
!(0, utils_3.isComponentAtPosition)(doc, tsDoc, originalPosition))) {
return null;
}
if ((0, documents_1.isInHTMLTagRange)(doc.html, doc.offsetAt(originalPosition)) ||
(0, svelte_ast_utils_1.isAttributeName)(svelteNode, 'Element') ||
(0, svelte_ast_utils_1.isEventHandler)(svelteNode, 'Element')) {
return null;
}
// If $store is renamed, only allow rename for $|store|
const text = tsDoc.getFullText();
if (text.charAt(renameInfo.triggerSpan.start) === '$') {
const definition = lang.getDefinitionAndBoundSpan(tsDoc.filePath, generatedOffset)
?.definitions?.[0];
if (definition && (0, utils_3.isTextSpanInGeneratedCode)(text, definition.textSpan)) {
renameInfo.triggerSpan.start++;
renameInfo.triggerSpan.length--;
renameInfo.isStore = true;
}
}
return renameInfo;
}
/**
* If the user renames a store variable, we need to rename the corresponding $store variables
* and vice versa.
*/
async enhanceRenamesInCaseOf$Store(renameLocations, newName, docs, lang) {
for (const loc of renameLocations) {
const snapshot = await docs.retrieve(loc.fileName);
if ((0, utils_3.isTextSpanInGeneratedCode)(snapshot.getFullText(), loc.textSpan)) {
if ((0, utils_3.isStoreVariableIn$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start)) {
// User renamed store, also rename corresponding $store locations
const storeRenameLocations = lang.findRenameLocations(snapshot.filePath, (0, utils_3.get$storeOffsetOf$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start), false, false, true);
return await this.mapAndFilterRenameLocations(storeRenameLocations, docs, `$${newName}`);
}
else if ((0, utils_3.is$storeVariableIn$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start)) {
// User renamed $store, also rename corresponding store
const storeRenameLocations = lang.findRenameLocations(snapshot.filePath, (0, utils_3.getStoreOffsetOf$storeDeclaration)(snapshot.getFullText(), loc.textSpan.start), false, false, true);
return await this.mapAndFilterRenameLocations(storeRenameLocations, docs);
// TODO once we allow providePrefixAndSuffixTextForRename to be configurable,
// we need to add one more step to update all other $store usages in other files
}
}
}
return [];
}
/**
* If user renames prop of component A inside component A,
* we need to handle the rename of the prop of A ourselves.
* Reason: the rename will do {oldPropName: newPropName}, meaning
* the rename will not propagate further, so we have to handle
* the conversion to {newPropName: newPropName} ourselves.
*/
async getAdditionLocationsForRenameOfPropInsideComponentWithProp(document, tsDoc, position, convertedRenameLocations, snapshots, lang, newName) {
// First find out if it's really the "rename prop inside component with that prop" case
// Use original document for that because only there the `export` is present.
// ':' for typescript's type operator (`export let bla: boolean`)
// '//' and '/*' for comments (`export let bla// comment` or `export let bla/* comment */`)
const regex = new RegExp(`export\\s+let\\s+${this.getVariableAtPosition(tsDoc, lang, position)}($|\\s|;|:|\/\*|\/\/)`);
const isRenameInsideComponentWithProp = regex.test((0, documents_1.getLineAtPosition)(position, document.getText()));
if (!isRenameInsideComponentWithProp) {
return [];
}
// We now know that the rename happens at `export let X` -> let's find the corresponding
// prop rename further below in the document.
const updatePropLocation = this.findLocationWhichWantsToUpdatePropName(convertedRenameLocations, snapshots);
if (!updatePropLocation) {
return [];
}
// Typescript does a rename of `oldPropName: newPropName` -> find oldPropName and rename that, too.
const idxOfOldPropName = tsDoc
.getFullText()
.lastIndexOf(':', updatePropLocation.textSpan.start);
// This requires svelte2tsx to have the properties written down like `return props: {bla: bla}`.
// It would not work for `return props: {bla}` because then typescript would do a rename of `{bla: renamed}`,
// so other locations would not be affected.
const replacementsForProp = (lang.findRenameLocations(updatePropLocation.fileName, idxOfOldPropName, false, false, true) || []).filter((rename) =>
// filter out all renames inside the component except the prop rename,
// because the others were done before and then would show up twice, making a wrong rename.
rename.fileName !== updatePropLocation.fileName ||
this.isInSvelte2TsxPropLine(tsDoc, rename));
const renameLocations = await this.mapAndFilterRenameLocations(replacementsForProp, snapshots);
// Adjust shorthands
return renameLocations.map((location) => {
if (updatePropLocation.fileName === location.fileName) {
return location;
}
const sourceFile = lang.getProgram()?.getSourceFile(location.fileName);
if (!sourceFile ||
location.fileName !== sourceFile.fileName ||
location.range.start.line < 0 ||
location.range.end.line < 0) {
return location;
}
const snapshot = snapshots.get(location.fileName);
if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
return location;
}
const shorthandLocation = this.transformShorthand(snapshot, location, newName);
return shorthandLocation || location;
});
}
/**
* If user renames prop of component A inside component B,
* we need to handle the rename of the prop of A ourselves.
* Reason: the rename will rename the prop in the computed svelte2tsx code,
* but not the `export let X` code in the original because the
* rename does not propagate further than the prop.
* This additional logic/propagation is done in this method.
*/
async getAdditionalLocationsForRenameOfPropInsideOtherComponent(convertedRenameLocations, snapshots, lang, requestedFileName) {
// Check if it's a prop rename
const updatePropLocation = this.findLocationWhichWantsToUpdatePropName(convertedRenameLocations, snapshots);
if (!updatePropLocation) {
return [];
}
const getCanonicalFileName = (0, utils_1.createGetCanonicalFileName)(typescript_1.default.sys.useCaseSensitiveFileNames);
if (getCanonicalFileName(updatePropLocation.fileName) ===
getCanonicalFileName(requestedFileName)) {
return [];
}
// Find generated `export let`
const doc = snapshots.get(updatePropLocation.fileName);
const match = this.matchGeneratedExportLet(doc, updatePropLocation);
if (!match) {
return [];
}
// Use match to replace that let, too.
const idx = (match.index || 0) + match[0].lastIndexOf(match[1]);
const replacementsForProp = lang.findRenameLocations(updatePropLocation.fileName, idx, false, false) || [];
return this.checkShortHandBindingOrSlotLetLocation(lang, await this.mapAndFilterRenameLocations(replacementsForProp, snapshots), snapshots);
}
// --------> svelte2tsx?
matchGeneratedExportLet(snapshot, updatePropLocation) {
const regex = new RegExp(
// no 'export let', only 'let', because that's what it's translated to in svelte2tsx
// '//' and '/*' for comments (`let bla/*Ωignore_startΩ*/`)
`\\s+let\\s+(${snapshot
.getFullText()
.substring(updatePropLocation.textSpan.start, updatePropLocation.textSpan.start + updatePropLocation.textSpan.length)})($|\\s|;|:|\/\*|\/\/)`);
const match = snapshot.getFullText().match(regex);
return match;
}
findLocationWhichWantsToUpdatePropName(convertedRenameLocations, snapshots) {
return convertedRenameLocations.find((loc) => {
// Props are not in mapped range
if (loc.range.start.line >= 0 && loc.range.end.line >= 0) {
return;
}
const snapshot = snapshots.get(loc.fileName);
// Props are in svelte snapshots only
if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
return false;
}
return this.isInSvelte2TsxPropLine(snapshot, loc);
});
}
// --------> svelte2tsx?
isInSvelte2TsxPropLine(snapshot, loc) {
return (0, utils_3.isAfterSvelte2TsxPropsReturn)(snapshot.getFullText(), loc.textSpan.start);
}
/**
* The rename locations the ts language services hands back are relative to the
* svelte2tsx generated code -> map it back to the original document positions.
* Some of those positions could be unmapped (line=-1), these are handled elsewhere.
* Also filter out wrong renames.
*/
async mapAndFilterRenameLocations(renameLocations, snapshots, newName) {
const mappedLocations = await Promise.all(renameLocations.map(async (loc) => {
const snapshot = await snapshots.retrieve(loc.fileName);
if (!(0, utils_3.isTextSpanInGeneratedCode)(snapshot.getFullText(), loc.textSpan)) {
return {
...loc,
range: this.mapRangeToOriginal(snapshot, loc.textSpan),
newName
};
}
}));
return this.filterWrongRenameLocations(mappedLocations.filter(utils_1.isNotNullOrUndefined));
}
filterWrongRenameLocations(mappedLocations) {
return (0, utils_1.filterAsync)(mappedLocations, async (loc) => {
const snapshot = await this.getSnapshot(loc.fileName);
if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
return true;
}
const content = snapshot.getText(0, snapshot.getLength());
// When the user renames a Svelte component, ts will also want to rename
// `__sveltets_2_instanceOf(TheComponentToRename)` or
// `__sveltets_1_ensureType(TheComponentToRename,..`. Prevent that.
// Additionally, we cannot rename the hidden variable containing the store value
return (notPrecededBy('__sveltets_2_instanceOf(') &&
notPrecededBy('__sveltets_1_ensureType(') && // no longer necessary for new transformation
notPrecededBy('= __sveltets_2_store_get('));
function notPrecededBy(str) {
return (content.lastIndexOf(str, loc.textSpan.start) !== loc.textSpan.start - str.length);
}
});
}
mapRangeToOriginal(snapshot, textSpan) {
// We need to work around a current svelte2tsx limitation: Replacements and
// source mapping is done in such a way that sometimes the end of the range is unmapped
// and the index of the last character is returned instead (which is one less).
// Most of the time this is not much of a problem, but in the context of renaming, it is.
// We work around that by adding +1 to the end, if necessary.
// This can be done because
// 1. we know renames can only ever occur in one line
// 2. the generated svelte2tsx code will not modify variable names, so we know
// the original range should be the same length as the textSpan's length
const range = (0, documents_1.mapRangeToOriginal)(snapshot, (0, utils_2.convertRange)(snapshot, textSpan));
if (range.end.character - range.start.character < textSpan.length) {
range.end.character++;
}
return range;
}
getVariableAtPosition(tsDoc, lang, position) {
const offset = tsDoc.offsetAt(tsDoc.getGeneratedPosition(position));
const { start, length } = lang.getSmartSelectionRange(tsDoc.filePath, offset).textSpan;
return tsDoc.getText(start, start + length);
}
async getLSAndTSDoc(document) {
return this.lsAndTsDocResolver.getLSAndTSDoc(document);
}
getSnapshot(filePath) {
return this.lsAndTsDocResolver.getOrCreateSnapshot(filePath);
}
checkShortHandBindingOrSlotLetLocation(lang, renameLocations, snapshots, newName) {
return renameLocations.map((location) => {
const sourceFile = lang.getProgram()?.getSourceFile(location.fileName);
if (!sourceFile ||
location.fileName !== sourceFile.fileName ||
location.range.start.line < 0 ||
location.range.end.line < 0) {
return location;
}
const snapshot = snapshots.get(location.fileName);
if (!(snapshot instanceof DocumentSnapshot_1.SvelteDocumentSnapshot)) {
return location;
}
const { parent } = snapshot;
if (snapshot.isSvelte5Plus && newName && location.suffixText) {
// Svelte 5 runes mode, thanks to $props(), is much easier to handle rename-wise.
// Notably, it doesn't need the "additional props rename locations" logic, because
// these renames already appear here.
const shorthandLocation = this.transformShorthand(snapshot, location, newName);
if (shorthandLocation) {
return shorthandLocation;
}
}
let rangeStart = parent.offsetAt(location.range.start);
let prefixText = location.prefixText?.trimRight();
// rename needs to be prefixed in case of a bind shorthand on a HTML element
if (!prefixText) {
const original = parent.getText({
start: vscode_languageserver_1.Position.create(location.range.start.line, location.range.start.character - bind.length),
end: location.range.end
});
if (original.startsWith(bind) &&
(0, documents_1.getNodeIfIsInHTMLStartTag)(parent.html, rangeStart)) {
return {
...location,
prefixText: original.slice(bind.length) + '={',
suffixText: '}'
};
}
}
if (!prefixText || prefixText.slice(-1) !== ':') {
return location;
}
// prefix is of the form `oldVarName: ` -> hints at a shorthand
// we need to make sure we only adjust shorthands on elements/components
if (!(0, documents_1.getNodeIfIsInStartTag)(parent.html, rangeStart) ||
// shorthands: let:xx, bind:xx, {xx}
(parent.getText().charAt(rangeStart - 1) !== ':' &&
// not use:action={{foo}}
!/[^{]\s+{$/.test(parent.getText({
start: vscode_languageserver_1.Position.create(0, 0),
end: location.range.start
})))) {
return location;
}
prefixText = prefixText.slice(0, -1) + '={';
location = {
...location,
prefixText,
suffixText: '}'
};
// rename range needs to be adjusted in case of an attribute shorthand
if (snapshot.getOriginalText().charAt(rangeStart - 1) === '{') {
rangeStart--;
const rangeEnd = parent.offsetAt(location.range.end) + 1;
location.range = {
start: parent.positionAt(rangeStart),
end: parent.positionAt(rangeEnd)
};
}
return location;
});
}
transformShorthand(snapshot, location, newName) {
const shorthand = this.getBindingOrAttrShorthand(snapshot, location.range.start);
if (shorthand) {
if (shorthand.isBinding) {
// bind:|foo| -> bind:|newName|={foo}
return {
...location,
prefixText: '',
suffixText: `={${shorthand.id.name}}`
};
}
else {
return {
...location,
range: {
// {|foo|} -> |{foo|}
start: {
line: location.range.start.line,
character: location.range.start.character - 1
},
end: location.range.end
},
// |{foo|} -> newName=|{foo|}
newName: shorthand.id.name,
prefixText: `${newName}={`,
suffixText: ''
};
}
}
}
getBindingOrAttrShorthand(snapshot, position, svelteNode = snapshot.svelteNodeAt(position)) {
if ((svelteNode?.parent?.type === 'Binding' ||
svelteNode?.parent?.type === 'AttributeShorthand') &&
svelteNode.parent.expression.end === svelteNode.parent.end) {
return {
id: svelteNode,
isBinding: svelteNode.parent.type === 'Binding'
};
}
}
}
exports.RenameProviderImpl = RenameProviderImpl;
//# sourceMappingURL=RenameProvider.js.map