UNPKG

svelte-language-server

Version:
984 lines 52 kB
"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