fish-lsp
Version:
LSP implementation for fish/fish-shell
205 lines (204 loc) • 11.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createRefactorAction = createRefactorAction;
exports.extractFunctionWithArgparseToCompletionsFile = extractFunctionWithArgparseToCompletionsFile;
exports.extractFunctionToFile = extractFunctionToFile;
exports.extractToFunction = extractToFunction;
exports.extractCommandToFunction = extractCommandToFunction;
exports.extractToVariable = extractToVariable;
exports.convertIfToCombiners = convertIfToCombiners;
const os_1 = __importDefault(require("os"));
const vscode_languageserver_1 = require("vscode-languageserver");
const tree_sitter_1 = require("../utils/tree-sitter");
const node_types_1 = require("../utils/node-types");
const action_kinds_1 = require("./action-kinds");
const combiner_1 = require("./combiner");
const path_1 = __importDefault(require("path"));
const translation_1 = require("../utils/translation");
const logger_1 = require("../logger");
const argparse_completions_1 = require("./argparse-completions");
/**
* Notice how this file compared to the other code-actions, uses a node as it's parameter
* This is because the reafactors are not based on diagnostics. However, if we need to use
* a diagnostic for some reason, we can always pass its `Document.data.node` property.
*
* This section is very much still a WIP, so there are definitely some improvements
* to be made.
*/
function createRefactorAction(title, kind, edits, preferredAction = false) {
return {
title,
kind,
edit: { changes: edits },
isPreferred: preferredAction,
};
}
function extractFunctionWithArgparseToCompletionsFile(document, range, node) {
logger_1.logger.log('extractFunctionWithArgparseToCompletionsFile', document, range, { node: { text: node.text, type: node.type } });
let selectedNode = node;
if ((0, node_types_1.isFunctionDefinitionName)(node)) {
selectedNode = node.parent;
}
if ((0, node_types_1.isCommandWithName)(selectedNode, 'argparse') || selectedNode.text.startsWith('argparse')) {
selectedNode = (0, tree_sitter_1.findEnclosingScope)(selectedNode);
}
if (selectedNode.type !== 'function_definition')
return;
const argparseNode = (0, tree_sitter_1.getChildNodes)(selectedNode).find(n => (0, node_types_1.isCommandWithName)(n, 'argparse'));
const hasArgparse = !!argparseNode;
if (!hasArgparse)
return;
const functionName = (0, tree_sitter_1.getChildNodes)(selectedNode).find(n => (0, node_types_1.isFunctionDefinitionName)(n)).text;
const autoloadType = document.getAutoloadType();
/** cancel if we're not in an autoloaded file */
if (functionName !== document.getAutoLoadName() || !['functions', 'config.fish'].includes(autoloadType))
return;
const completionPath = path_1.default.join(os_1.default.homedir(), '.config', 'fish', 'completions', `${functionName}.fish`);
const completionUri = (0, translation_1.pathToUri)(completionPath);
const completionFlags = (0, argparse_completions_1.findFlagsToComplete)(argparseNode);
const completionText = (0, argparse_completions_1.buildCompleteString)(functionName, completionFlags);
const changeAnnotation = {
label: `Create completions for '${functionName}' in file: ${completionPath}`,
description: `Create completions for '${functionName}' to file: ${completionPath}`,
};
const createFileAction = vscode_languageserver_1.CreateFile.create(completionUri, { ignoreIfExists: true, overwrite: false });
// Get the selected text
const selectedText = `\n# auto generated by fish-lsp\n${completionText}\n`;
const createFileEdit = vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.VersionedTextDocumentIdentifier.create(completionUri, 0), [vscode_languageserver_1.TextEdit.insert({ line: 0, character: 0 }, selectedText)]);
const workspaceEdit = {
documentChanges: [
createFileAction,
createFileEdit,
],
changeAnnotations: { [changeAnnotation.label]: changeAnnotation },
};
return {
title: `Create completions for '${functionName}' in file: ${completionPath}`,
kind: action_kinds_1.SupportedCodeActionKinds.RefactorExtract,
edit: workspaceEdit,
};
}
function extractFunctionToFile(document, range, node) {
logger_1.logger.log('extractFunctionToFile', document, range, { node: { text: node.text, type: node.type } });
let selectedNode = node;
if ((0, node_types_1.isFunctionDefinitionName)(node)) {
selectedNode = node.parent;
}
if (selectedNode.type !== 'function_definition')
return;
const functionName = (0, tree_sitter_1.getChildNodes)(selectedNode).find(n => (0, node_types_1.isFunctionDefinitionName)(n)).text;
// cancel if we're already in the file
if (functionName === document.getAutoLoadName())
return;
const functionPath = path_1.default.join(os_1.default.homedir(), '.config', 'fish', 'functions', `${functionName}.fish`);
const functionUri = (0, translation_1.pathToUri)(functionPath);
const changeAnnotation = {
label: `Extract function '${functionName}' to file: ${functionPath}`,
description: `Extract function '${functionName}' to file: ${functionPath}`,
};
const createFileAction = vscode_languageserver_1.CreateFile.create(functionUri, { ignoreIfExists: false, overwrite: true });
// Get the selected text
const selectedText = document.getText((0, tree_sitter_1.getRange)(selectedNode));
const createFileEdit = vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.VersionedTextDocumentIdentifier.create(functionUri, 0), [vscode_languageserver_1.TextEdit.insert({ line: 0, character: 0 }, selectedText)]);
const removeOldFunction = vscode_languageserver_1.TextDocumentEdit.create(vscode_languageserver_1.VersionedTextDocumentIdentifier.create(document.uri, document.version), [vscode_languageserver_1.TextEdit.del((0, tree_sitter_1.getRange)(selectedNode))]);
const workspaceEdit = {
documentChanges: [
createFileAction,
createFileEdit,
removeOldFunction,
],
changeAnnotations: { [changeAnnotation.label]: changeAnnotation },
};
return {
title: `Extract function '${functionName}' to file: ${functionPath}`,
kind: action_kinds_1.SupportedCodeActionKinds.RefactorExtract,
edit: workspaceEdit,
};
}
function extractToFunction(document, range) {
logger_1.logger.log('extractToFunction', document, range);
// Generate a unique function name
const functionName = `extracted_function_${Math.floor(Math.random() * 1000)}`;
// Get the selected text
const selectedText = document.getText(range);
// make sure we're not extracting nothing
if (selectedText.trim() === '' && document.getLine(range.start.line).trim() !== '')
return;
const indent = document.getIndentAtLine(range.start.line);
// Create the new function
const functionText = [
`\n${indent}function ${functionName}`,
...selectedText.split('\n').map(line => `${indent} ${line}`), // Indent the function body
`${indent}end\n`,
].join('\n');
// Insert the new function before the current scope
const insertEdit = vscode_languageserver_1.TextEdit.insert({ line: range.start.line, character: 0 }, `\n${functionText}\n`);
// Replace the selected text with a call to the new function
const replaceEdit = vscode_languageserver_1.TextEdit.replace(range, `${functionName}`);
return createRefactorAction(`Extract to local function '${functionName}'`, action_kinds_1.SupportedCodeActionKinds.RefactorExtract, {
[document.uri]: [replaceEdit, insertEdit],
});
}
function extractCommandToFunction(document, selectedNode) {
logger_1.logger.log('extractCommandToFunction', document, { selectedNode: { text: selectedNode.text, type: selectedNode.type } });
// Generate a unique function name
const functionName = `extracted_function_${Math.floor(Math.random() * 1000)}`;
let cmd = selectedNode;
if (selectedNode.type !== 'command') {
cmd = (0, node_types_1.findParentCommand)(selectedNode) || selectedNode;
}
if (!cmd || !(0, node_types_1.isCommand)(cmd))
return;
// Get the selected text
const selectedText = document.getText((0, tree_sitter_1.getRange)(cmd));
// Create the new function
const functionText = [
`\nfunction ${functionName}`,
...selectedText.split('\n').map(line => ` ${line}`), // Indent the function body
'end\n',
].join('\n');
// Replace the selected text with a call to the new function
const replaceEdit = vscode_languageserver_1.TextEdit.replace((0, tree_sitter_1.getRange)(cmd), `${functionName}`);
// Insert the new function before the current scope
// const insertPosition = getRange(selectedNode).start;
const insertEdit = vscode_languageserver_1.TextEdit.insert({ line: document.getLines(), character: 0 }, `\n${functionText}\n`);
return createRefactorAction(`Extract command to local function '${functionName}'`, action_kinds_1.SupportedCodeActionKinds.RefactorExtract, {
[document.uri]: [replaceEdit, insertEdit],
});
}
function extractToVariable(document, range, selectedNode) {
logger_1.logger.log('extractToVariable', document, { selectedNode: { text: selectedNode.text, type: selectedNode.type } });
// Only allow extracting commands or expressions
if (!(0, node_types_1.isCommand)(selectedNode))
return undefined;
const selectedText = document.getText(range);
const varName = `extracted_var_${Math.floor(Math.random() * 1000)}`;
// Create variable declaration
const declaration = `set -l ${varName} (${selectedText})\n`;
// Replace original text with variable
const replaceEdit = vscode_languageserver_1.TextEdit.replace(range, declaration);
return createRefactorAction(`Extract selected '${selectedNode.firstNamedChild.text}' command to local variable '${varName}'`, action_kinds_1.SupportedCodeActionKinds.RefactorExtract, {
[document.uri]: [replaceEdit],
});
}
function convertIfToCombiners(document, selectedNode, isSelected = true) {
logger_1.logger.log('convertIfToCombiners', document, { selectedNode: { text: selectedNode.text, type: selectedNode.type } });
let node = selectedNode;
if (node.type === 'if' && !(0, node_types_1.isIfStatement)(node)) {
node = node.parent;
}
if (!(0, node_types_1.isIfStatement)(node))
return undefined;
const combinerString = (0, combiner_1.convertIfToCombinersString)(node);
// format the input with proper indentation, trimStart() because the range will include the leading whitespace
const formattedString = (0, translation_1.formatTextWithIndents)(document, selectedNode.startPosition.row, combinerString).trimStart();
const message = isSelected ?
`Convert selected if statement to conditionally executed statement (line: ${node.startPosition.row + 1})` :
`Convert if statement to conditionally executed statement (line: ${node.startPosition.row + 1})`;
return createRefactorAction(message, action_kinds_1.SupportedCodeActionKinds.RefactorRewrite, {
[document.uri]: [vscode_languageserver_1.TextEdit.replace((0, tree_sitter_1.getRange)(node), formattedString)],
}, true);
}