fish-lsp
Version:
LSP implementation for fish/fish-shell
528 lines (527 loc) • 21.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.createFixAllAction = createFixAllAction;
exports.handleMissingEndFix = handleMissingEndFix;
exports.handleExtraEndFix = handleExtraEndFix;
exports.handleSingleQuoteVarFix = handleSingleQuoteVarFix;
exports.handleTestCommandVariableExpansionWithoutString = handleTestCommandVariableExpansionWithoutString;
exports.getQuickFixes = getQuickFixes;
const vscode_languageserver_1 = require("vscode-languageserver");
const error_codes_1 = require("../diagnostics/error-codes");
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");
const analyze_1 = require("../analyze");
const tree_sitter_2 = require("../utils/tree-sitter");
const translation_1 = require("../utils/translation");
const node_types_2 = require("../utils/node-types");
function createQuickFix(title, diagnostic, edits) {
return {
title,
kind: action_kinds_1.SupportedCodeActionKinds.QuickFix.toString(),
isPreferred: true,
diagnostics: [diagnostic],
edit: { changes: edits },
};
}
function createFixAllAction(document, actions) {
if (actions.length === 0)
return undefined;
const fixableActions = actions.filter(action => {
return action.isPreferred && action.kind === action_kinds_1.SupportedCodeActionKinds.QuickFix;
});
for (const fixable of fixableActions) {
logger_1.logger.info('createFixAllAction', { fixable: fixable.title });
}
if (fixableActions.length === 0)
return undefined;
const resultEdits = {};
const diagnostics = [];
for (const action of fixableActions) {
if (!action.edit || !action.edit.changes)
continue;
const changes = action.edit.changes;
for (const uri of Object.keys(changes)) {
const edits = changes[uri];
if (!edits || edits.length === 0)
continue;
if (!resultEdits[uri]) {
resultEdits[uri] = [];
}
const oldEdits = resultEdits[uri];
if (edits && edits?.length > 0) {
if (!oldEdits.some(e => edits.find(newEdit => (0, tree_sitter_1.equalRanges)(e.range, newEdit.range)))) {
oldEdits.push(...edits);
resultEdits[uri] = oldEdits;
diagnostics.push(...action.diagnostics || []);
}
}
}
}
const allEdits = [];
for (const uri in resultEdits) {
const edits = resultEdits[uri];
if (!edits || edits.length === 0)
continue;
allEdits.push(...edits);
}
return {
title: `Fix all auto-fixable quickfixes (total fixes: ${allEdits.length}) (codes: ${diagnostics.map(d => d.code).join(', ')})`,
kind: action_kinds_1.SupportedCodeActionKinds.QuickFixAll,
diagnostics,
edit: {
changes: resultEdits,
},
data: {
isQuickFix: true,
documentUri: document.uri,
totalEdits: allEdits.length,
uris: Array.from(new Set(Object.keys(resultEdits))),
},
};
}
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.uri).rootNode;
const errNode = root.descendantForPosition({ row: diagnostic.range.start.line, column: diagnostic.range.start.character });
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) {
const edit = vscode_languageserver_1.TextEdit.del(diagnostic.range);
return createQuickFix('Remove extra "end"', diagnostic, {
[document.uri]: [edit],
});
}
function handleMissingQuietError(document, diagnostic) {
const edit = vscode_languageserver_1.TextEdit.insert(diagnostic.range.end, ' -q ');
return {
title: 'Add silence (-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,
};
}
function handleDotSourceCommand(document, diagnostic) {
const edit = vscode_languageserver_1.TextEdit.replace(diagnostic.range, 'source');
return {
title: 'Convert dot source command to source',
kind: action_kinds_1.SupportedCodeActionKinds.QuickFix,
diagnostics: [diagnostic],
edit: {
changes: {
[document.uri]: [edit],
},
},
isPreferred: true,
};
}
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) {
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) {
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 handleCompletionFilenameMismatch(diagnostic, node, document) {
const functionName = node.text;
const newUri = document.uri.replace(/[^/]+\.fish$/, `${functionName}.fish`);
if (document.getAutoloadType() !== 'completions') {
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 completion '${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,
};
}
const getNodeType = (node) => {
if ((0, node_types_2.isFunctionDefinitionName)(node)) {
return 'function';
}
if ((0, node_types_2.isArgparseVariableDefinitionName)(node)) {
return 'argparse';
}
if ((0, node_types_2.isAliasDefinitionName)(node)) {
return 'alias';
}
if ((0, node_types_2.isVariableDefinitionName)(node)) {
return 'variable';
}
return 'unknown';
};
function handleUnusedSymbol(diagnostic, node, document) {
const nodeType = getNodeType(node);
if (nodeType === 'unknown')
return undefined;
let scopeNode = node;
while (scopeNode && !(0, node_types_2.isFunctionDefinition)(scopeNode)) {
scopeNode = scopeNode.parent;
}
if (nodeType === 'function') {
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,
};
}
if (nodeType === 'argparse') {
const parentCommand = (0, node_types_2.findParentCommand)(node);
if (!parentCommand)
return undefined;
const changeAnnotation = vscode_languageserver_1.ChangeAnnotation.create(`Check if argparse variable ${node.text} is set`, true, `Check if argparse variable '${node.text}' is set, in file '${document.getFilePath()}' (line: ${node.startPosition.row + 1})`);
const symbol = analyze_1.analyzer.getDefinition(document, diagnostic.range.end);
if (!symbol)
return undefined;
const indent = document.getIndentAtLine(parentCommand.endPosition.row);
const name = symbol.aliasedNames.length > 0
? symbol.aliasedNames.reduce((longest, current) => current.length > longest.length ? current : longest, '')
: symbol.name;
const insertText = [
'\n',
`if set -ql ${name}`,
' ',
'end',
].map(line => `${indent}${line}`).join('\n');
let parentNode = symbol.node;
if (parentNode && parentNode.nextNamedSibling && (0, node_types_2.isConditionalCommand)(parentNode.nextNamedSibling)) {
while (parentNode && parentNode.nextNamedSibling && (0, node_types_2.isConditionalCommand)(parentNode.nextNamedSibling)) {
parentNode = parentNode.nextNamedSibling;
}
}
const workspaceEdit = {
changes: {
[document.uri]: [
vscode_languageserver_1.TextEdit.insert((0, tree_sitter_2.getRange)(parentNode).end, insertText),
],
},
changeAnnotations: {
[changeAnnotation.label]: changeAnnotation,
},
};
return {
title: `Use \`argparse ${node.text}\` variable '${name}' if it's set in '${symbol.parent?.name || (0, translation_1.uriToReadablePath)(document.uri)}'`,
kind: action_kinds_1.SupportedCodeActionKinds.QuickFix,
diagnostics: [diagnostic],
edit: workspaceEdit,
isPreferred: true,
};
}
return undefined;
}
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,
};
}
function handleConvertDeprecatedFishLsp(diagnostic, node, document) {
logger_1.logger.log({ name: 'handleConvertDeprecatedFishLsp', diagnostic: diagnostic.range, node: node.text });
const replaceText = node.text === 'fish_lsp_logfile' ? 'fish_lsp_log_file' : node.text;
const edit = vscode_languageserver_1.TextEdit.replace(diagnostic.range, replaceText);
const workspaceEdit = {
changes: {
[document.uri]: [edit],
},
};
return {
title: 'Convert deprecated environment variable name',
kind: action_kinds_1.SupportedCodeActionKinds.QuickFix,
diagnostics: [diagnostic],
edit: workspaceEdit,
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.uri);
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);
}
logger_1.logger.info('getQuickFixes', { code: diagnostic.code, message: diagnostic.message, node: node?.text });
switch (diagnostic.code) {
case error_codes_1.ErrorCodes.missingEnd:
action = handleMissingEndFix(document, diagnostic, analyzer);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.extraEnd:
action = handleExtraEndFix(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.missingQuietOption:
action = handleMissingQuietError(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.usedUnviersalDefinition:
action = handleUniversalVariable(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.dotSourceCommand:
action = handleDotSourceCommand(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.zeroIndexedArray:
action = handleZeroIndexedArray(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.singleQuoteVariableExpansion:
action = handleSingleQuoteVarFix(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.testCommandMissingStringCharacters:
action = handleTestCommandVariableExpansionWithoutString(document, diagnostic);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.autoloadedFunctionMissingDefinition:
if (!node)
return [];
return [handleMissingDefinition(diagnostic, node, document)];
case error_codes_1.ErrorCodes.autoloadedFunctionFilenameMismatch:
if (!node)
return [];
action = handleFilenameMismatch(diagnostic, node, document);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.functionNameUsingReservedKeyword:
if (!node)
return [];
return [handleReservedKeyword(diagnostic, node, document)];
case error_codes_1.ErrorCodes.unusedLocalDefinition:
if (!node)
return [];
action = handleUnusedSymbol(diagnostic, node, document);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.autoloadedCompletionMissingCommandName:
if (!node)
return [];
action = handleCompletionFilenameMismatch(diagnostic, node, document);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.argparseMissingEndStdin:
action = handleAddEndStdinToArgparse(diagnostic, document);
if (action)
actions.push(action);
return actions;
case error_codes_1.ErrorCodes.fishLspDeprecatedEnvName:
if (!node)
return [];
return [handleConvertDeprecatedFishLsp(diagnostic, node, document)];
default:
return actions;
}
}