import { walk } from 'estree-walker'; import { EOL } from 'os'; import { Ast } from 'svelte/types/compiler/interfaces'; import { CodeAction, CodeActionKind, Diagnostic, DiagnosticSeverity, OptionalVersionedTextDocumentIdentifier, Position, TextDocumentEdit, TextEdit } from 'vscode-languageserver'; import { getLineOffsets, mapObjWithRangeToOriginal, offsetAt, positionAt } from '../../../../lib/documents'; import { getIndent, pathToUrl } from '../../../../utils'; import { SvelteDocument } from '../../SvelteDocument'; import ts from 'typescript'; // estree does not have start/end in their public Node interface, // but the AST returned by svelte/compiler does. Type as any as a workaround. type Node = any; /** * Get applicable quick fixes. */ export async function getQuickfixActions( svelteDoc: SvelteDocument, svelteDiagnostics: Diagnostic[] ) { const { ast } = await svelteDoc.getCompiled(); return Promise.all( svelteDiagnostics.map( async (diagnostic) => await createQuickfixAction(diagnostic, svelteDoc, ast) ) ); } async function createQuickfixAction( diagnostic: Diagnostic, svelteDoc: SvelteDocument, ast: Ast ): Promise<CodeAction> { const textDocument = OptionalVersionedTextDocumentIdentifier.create( pathToUrl(svelteDoc.getFilePath()), null ); return CodeAction.create( getCodeActionTitle(diagnostic), { documentChanges: [ TextDocumentEdit.create(textDocument, [ await getSvelteIgnoreEdit(svelteDoc, ast, diagnostic) ]) ] }, CodeActionKind.QuickFix ); } function getCodeActionTitle(diagnostic: Diagnostic) { // make it distinguishable with eslint's code action return `(svelte) Disable ${diagnostic.code} for this line`; } /** * Whether or not the given diagnostic can be ignored via a * <!-- svelte-ignore <code> --> */ export function isIgnorableSvelteDiagnostic(diagnostic: Diagnostic) { const { source, severity, code } = diagnostic; return ( code && !nonIgnorableWarnings.includes(<string>code) && source === 'svelte' && severity !== DiagnosticSeverity.Error ); } const nonIgnorableWarnings = [ 'missing-custom-element-compile-options', 'unused-export-let', 'css-unused-selector' ]; async function getSvelteIgnoreEdit(svelteDoc: SvelteDocument, ast: Ast, diagnostic: Diagnostic) { const { code, range: { start, end } } = diagnostic; const transpiled = await svelteDoc.getTranspiled(); const content = transpiled.getText(); const lineOffsets = getLineOffsets(content); const { html } = ast; const generatedStart = transpiled.getGeneratedPosition(start); const generatedEnd = transpiled.getGeneratedPosition(end); const diagnosticStartOffset = offsetAt(generatedStart, content, lineOffsets); const diagnosticEndOffset = offsetAt(generatedEnd, content, lineOffsets); const offsetRange: ts.TextRange = { pos: diagnosticStartOffset, end: diagnosticEndOffset }; const node = findTagForRange(html, offsetRange); const nodeStartPosition = positionAt(node.start, content, lineOffsets); const nodeLineStart = offsetAt( { line: nodeStartPosition.line, character: 0 }, content, lineOffsets ); const afterStartLineStart = content.slice(nodeLineStart); const indent = getIndent(afterStartLineStart); // TODO: Make all code action's new line consistent const ignore = `${indent}<!-- svelte-ignore ${code} -->${EOL}`; const position = Position.create(nodeStartPosition.line, 0); return mapObjWithRangeToOriginal(transpiled, TextEdit.insert(position, ignore)); } const elementOrComponent = ['Component', 'Element', 'InlineComponent']; function findTagForRange(html: Node, range: ts.TextRange) { let nearest = html; walk(html, { enter(node, parent) { const { type } = node; const isBlock = 'block' in node || node.type.toLowerCase().includes('block'); const isFragment = type === 'Fragment'; const keepLooking = isFragment || elementOrComponent.includes(type) || isBlock; if (!keepLooking) { this.skip(); return; } if (within(node, range) && parent === nearest) { nearest = node; } } }); return nearest; } function within(node: Node, range: ts.TextRange) { return node.end >= range.end && node.start <= range.pos; }