fish-lsp
Version:
LSP implementation for fish/fish-shell
362 lines (361 loc) • 14.3 kB
JavaScript
;
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;
}
}