UNPKG

fish-lsp

Version:

LSP implementation for fish/fish-shell

362 lines (361 loc) 14.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.handleMissingEndFix = handleMissingEndFix; exports.handleExtraEndFix = handleExtraEndFix; exports.handleSingleQuoteVarFix = handleSingleQuoteVarFix; exports.handleTestCommandVariableExpansionWithoutString = handleTestCommandVariableExpansionWithoutString; exports.getQuickFixes = getQuickFixes; const vscode_languageserver_1 = require("vscode-languageserver"); const errorCodes_1 = require("../diagnostics/errorCodes"); const tree_sitter_1 = require("../utils/tree-sitter"); const node_types_1 = require("../diagnostics/node-types"); const action_kinds_1 = require("./action-kinds"); const logger_1 = require("../logger"); // import { createAliasInlineAction, createAliasSaveActionNewFile } from './alias-wrapper'; const tree_sitter_2 = require("../utils/tree-sitter"); const translation_1 = require("../utils/translation"); const node_types_2 = require("../utils/node-types"); /** * These quick-fixes are separated from the other diagnostic quick-fixes because * future work will involve adding significantly more complex * solutions here (atleast I hope. I definitely think fish uniquely has a lot * of potential for how advancded quickfixes could become eventually). * * The quick-fixes located at disable-actions.ts are mainly for simple disabling * of diagnostic messages. */ // Helper to create a QuickFix code action function createQuickFix(title, diagnostic, edits) { return { title, kind: action_kinds_1.SupportedCodeActionKinds.QuickFix.toString(), isPreferred: true, diagnostics: [diagnostic], edit: { changes: edits }, }; } /** * utility function to get the error node token */ function getErrorNodeToken(node) { const { text } = node; const startTokens = Object.keys(node_types_1.ErrorNodeTypes); for (const token of startTokens) { if (text.startsWith(token)) { return node_types_1.ErrorNodeTypes[token]; } } return undefined; } function handleMissingEndFix(document, diagnostic, analyzer) { const root = analyzer.getTree(document).rootNode; const errNode = root.descendantForPosition({ row: diagnostic.range.start.line, column: diagnostic.range.start.character }); // const err = root!.childForFieldName('ERROR')!; // const toSearch = getChildNodes(err).find(node => node.isError)!; const rawErrorNodeToken = getErrorNodeToken(errNode); if (!rawErrorNodeToken) return undefined; const endTokenWithNewline = rawErrorNodeToken === 'end' ? '\nend' : rawErrorNodeToken; return { title: `Add missing "${rawErrorNodeToken}"`, diagnostics: [diagnostic], kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, edit: { changes: { [document.uri]: [ vscode_languageserver_1.TextEdit.insert({ line: errNode.endPosition.row, character: errNode.endPosition.column, }, endTokenWithNewline), ], }, }, }; } function handleExtraEndFix(document, diagnostic) { // Simply delete the extra end const edit = vscode_languageserver_1.TextEdit.del(diagnostic.range); return createQuickFix('Remove extra "end"', diagnostic, { [document.uri]: [edit], }); } // Handle missing quiet option error function handleMissingQuietError(document, diagnostic) { // Add -q flag const edit = vscode_languageserver_1.TextEdit.insert(diagnostic.range.end, ' -q '); return { title: 'Add quiet (-q) flag', kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: { changes: { [document.uri]: [edit], }, }, command: { command: 'editor.action.formatDocument', title: 'Format Document', }, isPreferred: true, }; } function handleZeroIndexedArray(document, diagnostic) { return { title: 'Convert zero-indexed array to one-indexed array', kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: { changes: { [document.uri]: [ vscode_languageserver_1.TextEdit.del(diagnostic.range), vscode_languageserver_1.TextEdit.insert(diagnostic.range.start, '1'), ], }, }, isPreferred: true, }; } // fix cases like: -xU function handleUniversalVariable(document, diagnostic) { const text = document.getText(diagnostic.range); let newText = text.replace(/U/g, 'g'); newText = newText.replace(/--universal/g, '--global'); const edit = vscode_languageserver_1.TextEdit.replace({ start: diagnostic.range.start, end: diagnostic.range.end, }, newText); return { title: 'Convert universal scope to global scope', kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: { changes: { [document.uri]: [edit], }, }, isPreferred: true, }; } function handleSingleQuoteVarFix(document, diagnostic) { // Replace single quotes with double quotes const text = document.getText(diagnostic.range); const newText = text.replace(/'/g, '"').replace(/\$/g, '\\$'); const edit = vscode_languageserver_1.TextEdit.replace(diagnostic.range, newText); return { title: 'Convert to double quotes', kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: { changes: { [document.uri]: [edit], }, }, isPreferred: true, }; } function handleTestCommandVariableExpansionWithoutString(document, diagnostic) { return createQuickFix('Surround test string comparison with double quotes', diagnostic, { [document.uri]: [ vscode_languageserver_1.TextEdit.insert(diagnostic.range.start, '"'), vscode_languageserver_1.TextEdit.insert(diagnostic.range.end, '"'), ], }); } function handleMissingDefinition(diagnostic, node, document) { // Create function definition with filename const functionName = (0, translation_1.pathToRelativeFunctionName)(document.uri); const edit = { range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 }, }, newText: `function ${functionName}\n # TODO: Implement function\nend\n`, }; return { title: `Create function '${functionName}'`, kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: { changes: { [document.uri]: [edit], }, }, isPreferred: true, }; } function handleFilenameMismatch(diagnostic, node, document) { const functionName = node.text; const newUri = document.uri.replace(/[^/]+\.fish$/, `${functionName}.fish`); if (document.getAutoloadType() !== 'functions') { return; } const oldName = document.getAutoLoadName(); const oldFilePath = document.getFilePath(); const oldFilename = document.getFilename(); const newFilePath = (0, translation_1.uriToPath)(newUri); const annotation = vscode_languageserver_1.ChangeAnnotation.create(`rename ${oldFilename} to ${newUri.split('/').pop()}`, true, `Rename '${oldFilePath}' to '${newFilePath}'`); const workspaceEdit = { documentChanges: [ vscode_languageserver_1.RenameFile.create(document.uri, newUri, { ignoreIfExists: false, overwrite: true }), ], changeAnnotations: { [annotation.label]: annotation, }, }; return { title: `RENAME: '${oldFilename}' to '${functionName}.fish' (File missing function '${oldName}')`, kind: action_kinds_1.SupportedCodeActionKinds.RefactorRewrite, diagnostics: [diagnostic], edit: workspaceEdit, }; } function handleReservedKeyword(diagnostic, node, document) { const replaceText = `__${node.text}`; const changeAnnotation = vscode_languageserver_1.ChangeAnnotation.create(`rename ${node.text} to ${replaceText}`, true, `Rename reserved keyword function definition '${node.text}' to '${replaceText}' (line: ${node.startPosition.row + 1})`); const workspaceEdit = { changes: { [document.uri]: [ vscode_languageserver_1.TextEdit.replace((0, tree_sitter_2.getRange)(node), replaceText), ], }, changeAnnotations: { [changeAnnotation.label]: changeAnnotation, }, }; return { title: `Rename reserved keyword '${node.text}' to '${replaceText}' (line: ${node.startPosition.row + 1})`, kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], isPreferred: true, edit: workspaceEdit, }; } function handleUnusedFunction(diagnostic, node, document) { // Find the entire function definition to remove let scopeNode = node; while (scopeNode && !(0, node_types_2.isFunctionDefinition)(scopeNode)) { scopeNode = scopeNode.parent; } const changeAnnotation = vscode_languageserver_1.ChangeAnnotation.create(`Removed unused function ${node.text}`, true, `Removed unused function '${node.text}', in file '${document.getFilePath()}' (line: ${node.startPosition.row + 1} - ${node.endPosition.row + 1})`); const workspaceEdit = { changes: { [document.uri]: [ vscode_languageserver_1.TextEdit.del((0, tree_sitter_2.getRange)(scopeNode)), ], }, changeAnnotations: { [changeAnnotation.label]: changeAnnotation, }, }; return { title: `Remove unused function ${node.text} (line: ${node.startPosition.row + 1})`, kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: workspaceEdit, }; } function handleAddEndStdinToArgparse(diagnostic, document) { const edit = vscode_languageserver_1.TextEdit.insert(diagnostic.range.end, ' -- $argv'); return { title: 'Add end stdin ` -- $argv` to argparse', kind: action_kinds_1.SupportedCodeActionKinds.QuickFix, diagnostics: [diagnostic], edit: { changes: { [document.uri]: [edit], }, }, isPreferred: true, }; } async function getQuickFixes(document, diagnostic, analyzer) { if (!diagnostic.code) return []; logger_1.logger.log({ code: diagnostic.code, message: diagnostic.message, severity: diagnostic.severity, node: diagnostic.data.node.text, range: diagnostic.range, }); let action; const actions = []; const root = analyzer.getRootNode(document); let node = root; if (root) { node = (0, tree_sitter_1.getChildNodes)(root).find(n => n.startPosition.row === diagnostic.range.start.line && n.startPosition.column === diagnostic.range.start.character); } switch (diagnostic.code) { case errorCodes_1.ErrorCodes.missingEnd: action = handleMissingEndFix(document, diagnostic, analyzer); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.extraEnd: action = handleExtraEndFix(document, diagnostic); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.missingQuietOption: action = handleMissingQuietError(document, diagnostic); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.usedUnviersalDefinition: action = handleUniversalVariable(document, diagnostic); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.zeroIndexedArray: action = handleZeroIndexedArray(document, diagnostic); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.singleQuoteVariableExpansion: action = handleSingleQuoteVarFix(document, diagnostic); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.testCommandMissingStringCharacters: action = handleTestCommandVariableExpansionWithoutString(document, diagnostic); if (action) actions.push(action); return actions; // case ErrorCodes.usedAlias: // if (!node) return []; // actions.push( // ...await Promise.all([ // createAliasInlineAction(node, document), // createAliasSaveActionNewFile(node, document), // ]), // ); // return actions; case errorCodes_1.ErrorCodes.autoloadedFunctionMissingDefinition: if (!node) return []; return [handleMissingDefinition(diagnostic, node, document)]; case errorCodes_1.ErrorCodes.autoloadedFunctionFilenameMismatch: if (!node) return []; action = handleFilenameMismatch(diagnostic, node, document); if (action) actions.push(action); return actions; case errorCodes_1.ErrorCodes.functionNameUsingReservedKeyword: if (!node) return []; return [handleReservedKeyword(diagnostic, node, document)]; case errorCodes_1.ErrorCodes.unusedLocalFunction: if (!node) return []; return [handleUnusedFunction(diagnostic, node, document)]; case errorCodes_1.ErrorCodes.argparseMissingEndStdin: action = handleAddEndStdinToArgparse(diagnostic, document); if (action) actions.push(action); return actions; default: return actions; } }