fish-lsp
Version:
LSP implementation for fish/fish-shell
242 lines (212 loc) • 8.25 kB
text/typescript
import {
MarkupContent,
SignatureHelp,
SignatureInformation,
} from 'vscode-languageserver';
import { SyntaxNode } from 'web-tree-sitter';
import { ExtendedBaseJson, PrebuiltDocumentationMap } from './utils/snippets';
import { FishAliasCompletionItem } from './utils/completion/types';
import * as NodeTypes from './utils/node-types';
import * as TreeSitter from './utils/tree-sitter';
import { CompletionItemMap } from './utils/completion/startup-cache';
export function buildSignature(label: string, value: string) : SignatureInformation {
return {
label: label,
documentation: {
kind: 'markdown',
value: value,
},
};
}
export function getCurrentNodeType(input: string) {
const prebuiltTypes = 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;
}
export function lineSignatureBuilder(lineRootNode: SyntaxNode, lineCurrentNode: SyntaxNode, _completeMmap: CompletionItemMap): SignatureHelp | null {
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;
}
}
export function getPipes(rootNode: SyntaxNode): ExtendedBaseJson[] {
const pipeNames = PrebuiltDocumentationMap.getByType('pipe');
return TreeSitter.getChildNodes(rootNode).reduce((acc: ExtendedBaseJson[], node) => {
const pipe = pipeNames.find(p => p.name === node.text);
if (pipe) acc.push(pipe);
return acc;
}, []);
}
function getVariableNode(rootNode: SyntaxNode): SyntaxNode | undefined {
return TreeSitter.getChildNodes(rootNode).find(c => NodeTypes.isVariableDefinition(c));
}
function getAllCommands(rootNode: SyntaxNode): SyntaxNode[] {
return TreeSitter.getChildNodes(rootNode).filter(c => NodeTypes.isCommand(c));
}
function getRegexOption(rootNode: SyntaxNode): SyntaxNode | undefined {
return TreeSitter.getChildNodes(rootNode).find(n => NodeTypes.isMatchingOption(n, { shortOption: '-r', longOption: '--regex' }));
}
function isStringWithRegex(line: string, regexOption: SyntaxNode | undefined): boolean {
return line.startsWith('string') && !!regexOption;
}
function isSetOrReadWithVarNode(line: string, varNode: SyntaxNode | undefined, rootNode: SyntaxNode, allCmds: SyntaxNode[]): boolean {
return !!varNode && (line.startsWith('set') || line.startsWith('read')) && allCmds.pop()?.text === rootNode.text.trim();
}
function getSignatureForVariable(varNode: SyntaxNode): SignatureHelp | null {
const output = getCurrentNodeType(varNode.text);
if (!output) return null;
return {
signatures: [buildSignature(output.name, output.description)],
activeSignature: 0,
activeParameter: 0,
};
}
function getReturnStatusSignature(): SignatureHelp {
const output = PrebuiltDocumentationMap.getByType('status').map((o: ExtendedBaseJson) => `___${o.name}___ - _${o.description}_`).join('\n');
return {
signatures: [buildSignature('$status', output)],
activeSignature: 0,
activeParameter: 0,
};
}
function getPipesSignature(pipes: ExtendedBaseJson[]): SignatureHelp {
return {
signatures: pipes.map((o: ExtendedBaseJson) => buildSignature(o.name, `${o.name} - _${o.description}_`)),
activeSignature: 0,
activeParameter: 0,
};
}
function getCommandSignature(firstCmd: SyntaxNode): SignatureHelp {
const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
return {
signatures: [buildSignature(firstCmd.text, output.map((o: ExtendedBaseJson) => `${o.name} - _${o.description}_`).join('\n'))],
activeSignature: 0,
activeParameter: 0,
};
}
export function getAliasedCompletionItemSignature(item: FishAliasCompletionItem): SignatureHelp {
// const output = PrebuiltDocumentationMap.getByType('command').filter(n => n.name === firstCmd.text);
return {
signatures: [buildSignature(item.label, [
'```fish',
`${item.fishKind} ${item.label} ${item.detail}`,
'```',
].join('\n'))],
activeSignature: 0,
activeParameter: 0,
};
}
export function regexStringSignature() : SignatureInformation {
//const regexItems = stringRegexExpressions;
//let signatureDoc = ["__String Regex Patterns__", "---"];
//for (const item of regexItems) {
// signatureDoc.push(`${item.label} {item.description}`)
//}
const signatureDoc: MarkupContent = {
kind: 'markdown',
value: [
markdownStringRepetitions,
markdownStringCharClasses,
markdownStringGroups,
].join('\n---\n'),
};
return {
label: 'Regex Patterns',
documentation: signatureDoc,
};
}
function regexStringCharacterSets(): SignatureInformation {
return {
label: 'Regex Groups',
documentation: {
kind: 'markdown',
value: markdownStringCharacterSets,
} as MarkupContent,
};
}
type signatureType = 'stringRegexPatterns' | 'stringRegexCharacterSets';
export const signatureIndex: {[str in signatureType]: number} = {
stringRegexPatterns: 0,
stringRegexCharacterSets: 1,
};
export function getDefaultSignatures() : SignatureHelp {
return {
activeParameter: 0,
activeSignature: 0,
signatures: [
regexStringSignature(),
regexStringCharacterSets(),
],
};
}
// REGEX STRING LINES
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');