UNPKG

svelte-language-server

Version:
921 lines 48 kB
"use strict"; 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