UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

216 lines (191 loc) 6.34 kB
// src/code-actions/disable-diagnostics.ts import { CodeAction, Diagnostic, DiagnosticSeverity, TextEdit } from 'vscode-languageserver'; import { LspDocument } from '../document'; import { ErrorCodes } from '../diagnostics/errorCodes'; import { SupportedCodeActionKinds } from './action-kinds'; import { logger } from '../logger'; interface DiagnosticGroup { startLine: number; endLine: number; diagnostics: Diagnostic[]; } function createDisableAction( title: string, document: LspDocument, edits: TextEdit[], diagnostics: Diagnostic[], isPreferred: boolean = false, ): CodeAction { return { title, kind: SupportedCodeActionKinds.Disable, edit: { changes: { [document.uri]: edits, }, }, diagnostics, isPreferred, }; } export function handleDisableSingleLine( document: LspDocument, diagnostic: Diagnostic, ): CodeAction { const indent = document.getIndentAtLine(diagnostic.range.start.line); // Insert disable comment above the diagnostic line const edit = TextEdit.insert( { line: diagnostic.range.start.line, character: 0 }, `${indent}# @fish-lsp-disable-next-line ${diagnostic.code}\n`, ); const severity = ErrorCodes.getSeverityString(diagnostic.severity); return createDisableAction( `Disable ${severity} diagnostic ${diagnostic.code} for line ${diagnostic.range.start.line + 1}`, document, [edit], [diagnostic], ); } export function handleDisableBlock( document: LspDocument, group: DiagnosticGroup, ): CodeAction { const numbers = Array.from(new Set(group.diagnostics.map(diagnostic => diagnostic.code)).values()).join(' '); const startIndent = document.getIndentAtLine(group.startLine); const endIndent = document.getIndentAtLine(group.endLine); const edits = [ // Insert disable comment at start of block TextEdit.insert( { line: group.startLine, character: 0 }, `${startIndent}# @fish-lsp-disable ${numbers}\n`, ), // Insert enable comment after end of block TextEdit.insert( { line: group.endLine + 1, character: 0 }, `${endIndent}# @fish-lsp-enable ${numbers}\n`, ), ]; return { ...createDisableAction( `Disable diagnostics ${numbers} in block (lines ${group.startLine + 1}-${group.endLine + 1})`, document, edits, group.diagnostics, ), }; } // Group diagnostics that are adjacent or within N lines of each other export function groupDiagnostics(diagnostics: Diagnostic[], maxGap: number = 1): DiagnosticGroup[] { if (diagnostics.length === 0) return []; // Sort diagnostics by starting line const sorted = [...diagnostics].sort((a, b) => a.range.start.line - b.range.start.line, ); const groups: DiagnosticGroup[] = []; let currentGroup: DiagnosticGroup = { startLine: sorted[0]!.range.start.line, endLine: sorted[0]!.range.end.line, diagnostics: [sorted[0]!], }; for (let i = 1; i < sorted.length; i++) { const current = sorted[i]!; const gap = current.range.start.line - currentGroup.endLine; if (gap <= maxGap) { // Add to current group currentGroup.endLine = Math.max(currentGroup.endLine, current.range.end.line); currentGroup.diagnostics.push(current); } else { // Start new group groups.push(currentGroup); currentGroup = { startLine: current.range.start.line, endLine: current.range.end.line, diagnostics: [current], }; } } // Add final group groups.push(currentGroup); return groups; } export function handleDisableEntireFile( document: LspDocument, diagnostics: Diagnostic[], ): CodeAction[] { const results: CodeAction[] = []; const diagnosticsCounts = new Map<keyof typeof ErrorCodes.allErrorCodes, number>(); diagnostics.forEach(diagnostic => { const code = diagnostic.code as keyof typeof ErrorCodes.allErrorCodes; diagnosticsCounts.set(code, (diagnosticsCounts.get(code) || 0) + 1); }); const matchingDiagnostics: Array<ErrorCodes.codeTypes> = []; diagnosticsCounts.forEach((count, code) => { if (count >= 5) { logger.log(`CODEACTION: Disabling ${count} ${code.toString()} diagnostics in file`); } matchingDiagnostics.push(code as ErrorCodes.codeTypes); }); if (matchingDiagnostics.length === 0) return results; let tokenLine = 0; let firstLine = document.getLine(tokenLine); if (firstLine.startsWith('#!/')) { tokenLine++; } firstLine = document.getLine(tokenLine); const allNumbersStr = matchingDiagnostics.join(' ').trim(); if (!firstLine.startsWith('# @fish-lsp-disable')) { const edits = [ TextEdit.insert( { line: tokenLine, character: 0 }, `# @fish-lsp-disable ${allNumbersStr}\n`, ), ]; results.push( createDisableAction( `Disable all diagnostics in file (${allNumbersStr.split(' ').join(', ')})`, document, edits, diagnostics, ), ); matchingDiagnostics.forEach(match => { const severity = ErrorCodes.getSeverityString(ErrorCodes.getDiagnostic(match).severity); results.push( createDisableAction( `Disable ${severity} ${match.toString()} diagnostics for entire file`, document, [ TextEdit.insert({ line: tokenLine, character: 0 }, `# @fish-lsp-disable ${match.toString()}\n`), ], diagnostics, ), ); }); } return results; } export function getDisableDiagnosticActions( document: LspDocument, diagnostics: Diagnostic[], ): CodeAction[] { const actions: CodeAction[] = []; // Add single-line disable actions for each diagnostic diagnostics .filter(diagnostic => diagnostic.severity === DiagnosticSeverity.Warning || diagnostic.code === ErrorCodes.sourceFileDoesNotExist, ).forEach(diagnostic => { actions.push(handleDisableSingleLine(document, diagnostic)); }); // Add block disable actions for groups const groups = groupDiagnostics(diagnostics); groups.forEach(group => { // Only create block actions for multiple diagnostics if (group.diagnostics.length > 1) { actions.push(handleDisableBlock(document, group)); } }); actions.push(...handleDisableEntireFile(document, diagnostics)); return actions; }