fish-lsp
Version:
LSP implementation for fish/fish-shell
403 lines (402 loc) • 15.7 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.signatureIndex = void 0;
exports.buildSignature = buildSignature;
exports.getCurrentNodeType = getCurrentNodeType;
exports.lineSignatureBuilder = lineSignatureBuilder;
exports.getPipes = getPipes;
exports.getAliasedCompletionItemSignature = getAliasedCompletionItemSignature;
exports.regexStringSignature = regexStringSignature;
exports.isMatchingOption = isMatchingOption;
exports.getActiveParameterIndex = getActiveParameterIndex;
exports.isRegexStringSignature = isRegexStringSignature;
exports.findActiveParameterStringRegex = findActiveParameterStringRegex;
exports.getDefaultSignatures = getDefaultSignatures;
exports.getFunctionSignatureHelp = getFunctionSignatureHelp;
const vscode_languageserver_1 = require("vscode-languageserver");
const snippets_1 = require("./utils/snippets");
const NodeTypes = __importStar(require("./utils/node-types"));
const TreeSitter = __importStar(require("./utils/tree-sitter"));
const options_1 = require("./parsing/options");
const markdown_builder_1 = require("./utils/markdown-builder");
const translation_1 = require("./utils/translation");
function buildSignature(label, value) {
return {
label: label,
documentation: {
kind: 'markdown',
value: value,
},
};
}
function getCurrentNodeType(input) {
const prebuiltTypes = snippets_1.PrebuiltDocumentationMap.getByName(input);
if (!prebuiltTypes || prebuiltTypes.length === 0) {
return null;
}
let longestDocs = prebuiltTypes[0];
for (const prebuilt of prebuiltTypes) {
if (prebuilt.description.length > longestDocs.description.length) {
longestDocs = prebuilt;
}
}
return longestDocs;
}
function lineSignatureBuilder(lineRootNode, lineCurrentNode, _completeMmap) {
const currentCmd = NodeTypes.findParentCommand(lineCurrentNode) || lineRootNode;
const pipes = getPipes(lineRootNode);
const varNode = getVariableNode(lineRootNode);
const allCmds = getAllCommands(lineRootNode);
const regexOption = getRegexOption(lineRootNode);
if (pipes.length === 1)
return getPipesSignature(pipes);
switch (true) {
case isStringWithRegex(currentCmd.text, regexOption):
return getDefaultSignatures();
case varNode && isSetOrReadWithVarNode(currentCmd?.text || lineRootNode.text, varNode, lineRootNode, allCmds):
return getSignatureForVariable(varNode);
case currentCmd?.text.startsWith('return') || lineRootNode.text.startsWith('return'):
return getReturnStatusSignature();
case allCmds.length === 1:
return getCommandSignature(currentCmd);
default:
return null;
}
}
function getPipes(rootNode) {
const pipeNames = snippets_1.PrebuiltDocumentationMap.getByType('pipe');
return TreeSitter.getChildNodes(rootNode).reduce((acc, node) => {
const pipe = pipeNames.find(p => p.name === node.text);
if (pipe)
acc.push(pipe);
return acc;
}, []);
}
function getVariableNode(rootNode) {
return TreeSitter.getChildNodes(rootNode).find(c => NodeTypes.isVariableDefinition(c));
}
function getAllCommands(rootNode) {
return TreeSitter.getChildNodes(rootNode).filter(c => NodeTypes.isCommand(c));
}
function getRegexOption(rootNode) {
return TreeSitter.getChildNodes(rootNode).find(n => NodeTypes.isMatchingOption(n, options_1.Option.create('-r', '--regex')));
}
function isStringWithRegex(line, regexOption) {
return line.startsWith('string') && !!regexOption;
}
function isSetOrReadWithVarNode(line, varNode, rootNode, allCmds) {
return !!varNode && (line.startsWith('set') || line.startsWith('read')) && allCmds.pop()?.text === rootNode.text.trim();
}
function getSignatureForVariable(varNode) {
const output = getCurrentNodeType(varNode.text);
if (!output)
return null;
return {
signatures: [buildSignature(output.name, output.description)],
activeSignature: 0,
activeParameter: 0,
};
}
function getReturnStatusSignature() {
const output = snippets_1.PrebuiltDocumentationMap.getByType('status').map((o) => `___${o.name}___ - _${o.description}_`).join('\n');
return {
signatures: [buildSignature('$status', output)],
activeSignature: 0,
activeParameter: 0,
};
}
function getPipesSignature(pipes) {
return {
signatures: pipes.map((o) => buildSignature(o.name, `${o.name} - _${o.description}_`)),
activeSignature: 0,
activeParameter: 0,
};
}
function getCommandSignature(firstCmd) {
const output = snippets_1.PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
return {
signatures: [buildSignature(firstCmd.text, output.map((o) => `${o.name} - _${o.description}_`).join('\n'))],
activeSignature: 0,
activeParameter: 0,
};
}
function getAliasedCompletionItemSignature(item) {
return {
signatures: [buildSignature(item.label, [
'```fish',
`${item.fishKind} ${item.label} ${item.detail}`,
'```',
].join('\n'))],
activeSignature: 0,
activeParameter: 0,
};
}
function regexStringSignature() {
const signatureDoc = {
kind: 'markdown',
value: [
markdownStringRepetitions,
markdownStringCharClasses,
markdownStringGroups,
].join('\n---\n'),
};
return {
label: 'Regex Patterns',
documentation: signatureDoc,
};
}
function regexStringCharacterSets() {
const inputText = [
markdownStringRepetitions,
markdownStringCharClasses,
markdownStringGroups,
].join('\n---\n');
const parameters = [
vscode_languageserver_1.ParameterInformation.create('argv[1]', inputText),
vscode_languageserver_1.ParameterInformation.create('argv[2]', inputText),
];
return {
label: 'Regex Groups',
documentation: {
kind: 'markdown',
value: markdownStringCharacterSets,
},
parameters: parameters,
activeParameter: 0,
};
}
function isMatchingOption(text, options) {
if (!text.startsWith('-'))
return false;
if (text.startsWith('--') && options.longOption) {
const cleanText = text.includes('=') ? text.slice(0, text.indexOf('=')) : text;
return cleanText === `--${options.longOption}`;
}
if (text.startsWith('-') && options.shortOption) {
return text.slice(1).includes(options.shortOption);
}
return false;
}
function getActiveParameterIndex(line, commandName, needsSubcommand, cursorPosition) {
const tokens = line.trim().split(/\s+/);
let currentPosition = 0;
let paramIndex = 0;
const commands = commandName.split(' ');
let previousWasCommand = false;
for (const token of tokens) {
if (commands.includes(token) || ['if', 'else if', 'switch', 'case'].includes(token)) {
cursorPosition += token.length + 1;
previousWasCommand = true;
continue;
}
if (needsSubcommand && previousWasCommand) {
cursorPosition += token.length + 1;
previousWasCommand = false;
continue;
}
break;
}
for (let i = 1; i < tokens.length; i++) {
const token = tokens[i];
if (currentPosition + token.length >= cursorPosition) {
break;
}
if (token.startsWith('-')) {
if (i + 1 < tokens.length && !tokens[i + 1].startsWith('-')) {
i++;
currentPosition += tokens[i].length + 1;
}
}
else {
paramIndex++;
}
currentPosition += token.length + 1;
}
return paramIndex;
}
function isRegexStringSignature(line) {
const tokens = line.split(' ');
const hasStringCommand = tokens.some(token => token === 'string') && !tokens.some(token => token === '--');
if (hasStringCommand) {
return tokens.some(token => isMatchingOption(token, {
shortOption: 'r',
longOption: 'regex',
}));
}
return false;
}
function findActiveParameterStringRegex(line, cursorPosition) {
const tokens = line.split(' ');
const hasStringCommand = tokens.some(token => token === 'string');
const isRegex = hasStringCommand && tokens.some(token => isMatchingOption(token, {
shortOption: 'r',
longOption: 'regex',
}));
const activeParameter = isRegex ? getActiveParameterIndex(line, 'string ', true, cursorPosition) : 0;
return { isRegex, activeParameter };
}
exports.signatureIndex = {
stringRegexPatterns: 0,
stringRegexCharacterSets: 1,
};
function getDefaultSignatures() {
return {
activeParameter: 0,
activeSignature: 0,
signatures: [
regexStringSignature(),
regexStringCharacterSets(),
],
};
}
function getFunctionSignatureHelp(analyzer, lineLastNode, line, position) {
const functionName = lineLastNode.parent?.firstNamedChild?.text.trim();
if (!functionName)
return null;
const funcSymbol = analyzer.findSymbol((symbol, _) => symbol.name === functionName);
if (!funcSymbol || funcSymbol.kind !== vscode_languageserver_1.SymbolKind.Function)
return null;
const paramNames = funcSymbol.children
.filter(s => s.fishKind === 'FUNCTION_VARIABLE' && s.name !== 'argv');
const argvParam = funcSymbol.children
.find(s => s.fishKind === 'FUNCTION_VARIABLE' && s.name === 'argv');
if (argvParam) {
paramNames.push(argvParam);
}
const paramDocs = paramNames.map((p, idx) => {
const markdownString = p.toMarkupContent().value.split(markdown_builder_1.md.separator());
const label = p.name === 'argv'
? `$${p.name}[${idx + 1}..-1]`
: p.name;
const newContentString = p.name === 'argv'
? [
'',
`${markdown_builder_1.md.bold(`(${(0, translation_1.symbolKindToString)(p.kind)})`)} ${label}`,
markdown_builder_1.md.separator(),
`This parameter corresponds to ${markdown_builder_1.md.inlineCode(`$argv[${idx + 1}..-1]`)} in the function.`,
'',
].join(markdown_builder_1.md.newline())
: [
'',
`${markdown_builder_1.md.bold(`(${(0, translation_1.symbolKindToString)(p.kind)})`)} ${markdown_builder_1.md.inlineCode(p.name)}`,
markdown_builder_1.md.separator(),
`This parameter corresponds to ${markdown_builder_1.md.inlineCode(`$argv[${idx + 1}]`)} in the function.`,
'',
].join(markdown_builder_1.md.newline());
const newValue = p.name === 'argv'
? [
newContentString,
].join(markdown_builder_1.md.separator())
: [
newContentString,
markdownString.slice(3, 4),
].join(markdown_builder_1.md.separator());
const newContent = {
kind: vscode_languageserver_1.MarkupKind.Markdown,
value: newValue,
};
return {
label: label,
documentation: newContent,
};
});
const label = `${funcSymbol.name} ${paramDocs.map(p => p.label).join(' ')}`.trim();
const signature = vscode_languageserver_1.SignatureInformation.create(label, funcSymbol.detail, ...paramDocs);
signature.documentation = {
kind: vscode_languageserver_1.MarkupKind.Markdown,
value: funcSymbol.detail || 'No documentation available',
};
const activeParameter = calculateActiveParameter(line, position) - 1;
return {
signatures: [signature],
activeSignature: 0,
activeParameter: Math.min(activeParameter, paramNames.length - 1),
};
}
function calculateActiveParameter(line, position) {
const textBeforeCursor = line.substring(0, position.character);
const tokens = textBeforeCursor.trim().split(/\s+/);
let paramCount = 0;
for (let i = 1; i < tokens.length; i++) {
const token = tokens[i];
if (token?.startsWith('-')) {
if (i + 1 < tokens.length && !tokens[i + 1]?.startsWith('-')) {
i++;
}
continue;
}
paramCount++;
}
return paramCount;
}
const markdownStringRepetitions = [
'Repetitions',
'-----------',
'- __*__ refers to 0 or more repetitions of the previous expression',
'- __+__ 1 or more',
'- __?__ 0 or 1.',
'- __{n}__ to exactly n (where n is a number)',
'- __{n,m}__ at least n, no more than m.',
'- __{n,}__ n or more',
].join('\n');
const markdownStringCharClasses = [
'Character Classes',
'-----------------',
'- __.__ any character except newline',
'- __\\d__ a decimal digit and __\\D__, not a decimal digit',
'- __\\s__ whitespace and __\\S__, not whitespace',
'- __\\w__ a “word” character and __\\W__, a “non-word” character',
'- __\\b__ a “word” boundary, and __\\B__, not a word boundary',
'- __[...]__ (where “…” is some characters) is a character set',
'- __[^...]__ is the inverse of the given character set',
'- __[x-y]__ is the range of characters from x-y',
'- __[[:xxx:]]__ is a named character set',
'- __[[:^xxx:]]__ is the inverse of a named character set',
].join('\n');
const markdownStringCharacterSets = [
'__[[:alnum:]]__ : “alphanumeric”',
'__[[:alpha:]]__ : “alphabetic”',
'__[[:ascii:]]__ : “0-127”',
'__[[:blank:]]__ : “space or tab”',
'__[[:cntrl:]]__ : “control character”',
'__[[:digit:]]__ : “decimal digit”',
'__[[:graph:]]__ : “printing, excluding space”',
'__[[:lower:]]__ : “lower case letter”',
'__[[:print:]]__ : “printing, including space”',
'__[[:punct:]]__ : “printing, excluding alphanumeric”',
'__[[:space:]]__ : “white space”',
'__[[:upper:]]__ : “upper case letter”',
'__[[:word:]]__ : “same as w”',
'__[[:xdigit:]]__ : “hexadecimal digit”',
].join('\n');
const markdownStringGroups = [
'Groups',
'------',
'- __(...)__ is a capturing group',
'- __(?:...)__ is a non-capturing group',
'- __\\n__ is a backreference (where n is the number of the group, starting with 1)',
'- __$n__ is a reference from the replacement expression to a group in the match expression.',
].join('\n');